├── src ├── test │ ├── resources │ │ ├── nextrtc.properties │ │ └── log4j.properties │ └── java │ │ └── org │ │ └── nextrtc │ │ └── signalingserver │ │ ├── performance │ │ ├── Tuple.java │ │ ├── README.md │ │ ├── PerformanceTest.java │ │ └── Peer.java │ │ ├── domain │ │ ├── LocalStreamCreated2.java │ │ ├── ServerEventCheck.java │ │ ├── resolver │ │ │ └── SpringSignalResolverTest.java │ │ ├── PingTaskTest.java │ │ ├── RTCConnectionsTest.java │ │ ├── TestClientActor.java │ │ ├── EventContentTest.java │ │ ├── MockedClient.java │ │ └── conversation │ │ │ └── BroadcastConversationTest.java │ │ ├── cases │ │ ├── OfferResponseHandlerTest.java │ │ ├── ExchangeSignalsBetweenMembersTest.java │ │ ├── LeftConversationTest.java │ │ ├── TextMessageTest.java │ │ ├── JoinConversationTest.java │ │ └── CreateConversationTest.java │ │ ├── TestConfig.java │ │ ├── EventChecker.java │ │ ├── repository │ │ ├── MembersTest.java │ │ └── ConversationsTest.java │ │ ├── eventbus │ │ ├── EventDispatcherTest.java │ │ └── EventBusTest.java │ │ ├── MessageMatcher.java │ │ ├── factory │ │ └── ConversationFactoryTest.java │ │ ├── api │ │ └── DecoderTest.java │ │ └── BaseTest.java └── main │ ├── java │ └── org │ │ └── nextrtc │ │ └── signalingserver │ │ ├── domain │ │ ├── MessageSender.java │ │ ├── Connection.java │ │ ├── SignalResolver.java │ │ ├── resolver │ │ │ ├── ManualSignalResolver.java │ │ │ ├── SpringSignalResolver.java │ │ │ └── AbstractSignalResolver.java │ │ ├── PingTask.java │ │ ├── conversation │ │ │ ├── MeshConversation.java │ │ │ ├── MeshWithMasterConversation.java │ │ │ ├── AbstractMeshConversation.java │ │ │ └── BroadcastConversation.java │ │ ├── Message.java │ │ ├── Signals.java │ │ ├── CloseableContext.java │ │ ├── RTCConnections.java │ │ ├── DefaultMessageSender.java │ │ ├── Member.java │ │ ├── Conversation.java │ │ ├── InternalMessage.java │ │ ├── Signal.java │ │ ├── EventContext.java │ │ └── Server.java │ │ ├── cases │ │ ├── SignalHandler.java │ │ ├── CandidateHandler.java │ │ ├── OfferResponseHandler.java │ │ ├── AnswerResponseHandler.java │ │ ├── Exchange.java │ │ ├── connection │ │ │ ├── ConnectionState.java │ │ │ └── ConnectionContext.java │ │ ├── TextMessage.java │ │ ├── ExchangeSignalsBetweenMembers.java │ │ ├── CreateConversation.java │ │ ├── LeftConversation.java │ │ ├── RegisterMember.java │ │ └── JoinConversation.java │ │ ├── api │ │ ├── NextRTCHandler.java │ │ ├── dto │ │ │ ├── NextRTCMember.java │ │ │ ├── NextRTCConversation.java │ │ │ └── NextRTCEvent.java │ │ ├── ConfigurationBuilder.java │ │ ├── annotation │ │ │ └── NextRTCEventListener.java │ │ ├── NextRTCEventBus.java │ │ ├── EndpointConfiguration.java │ │ ├── NextRTCServer.java │ │ └── NextRTCEvents.java │ │ ├── factory │ │ ├── ConversationFactory.java │ │ ├── ConnectionContextFactory.java │ │ ├── MemberFactory.java │ │ ├── SpringConnectionContextFactory.java │ │ ├── SpringMemberFactory.java │ │ ├── ManualMemberFactory.java │ │ ├── ManualConnectionContextFactory.java │ │ ├── AbstractConversationFactory.java │ │ ├── SpringConversationFactory.java │ │ └── ManualConversationFactory.java │ │ ├── property │ │ ├── NextRTCProperties.java │ │ ├── ManualNextRTCProperties.java │ │ └── SpringNextRTCProperties.java │ │ ├── eventbus │ │ ├── EventDispatcher.java │ │ ├── EventBusSetup.java │ │ ├── SpringEventDispatcher.java │ │ ├── ManualEventDispatcher.java │ │ └── AbstractEventDispatcher.java │ │ ├── repository │ │ ├── MemberRepository.java │ │ ├── ConversationRepository.java │ │ ├── Members.java │ │ └── Conversations.java │ │ ├── Names.java │ │ ├── exception │ │ ├── Exceptions.java │ │ └── SignalingException.java │ │ ├── modules │ │ ├── NextRTCRepositories.java │ │ ├── NextRTCMedia.java │ │ ├── NextRTCFactories.java │ │ ├── NextRTCBeans.java │ │ └── NextRTCSignals.java │ │ ├── NextRTCComponent.java │ │ └── NextRTCConfig.java │ └── resources │ └── nextrtc.properties ├── .gitignore ├── LICENSE └── CHANGELOG /src/test/resources/nextrtc.properties: -------------------------------------------------------------------------------- 1 | nextrtc.ping.timeout=3 2 | nextrtc.join_only_to_existing=false -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/MessageSender.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | public interface MessageSender { 4 | void send(InternalMessage message); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/nextrtc.properties: -------------------------------------------------------------------------------- 1 | #time in seconds 2 | nextrtc.ping_period=3 3 | nextrtc.scheduler_size=10 4 | nextrtc.max_connection_setup_time=30 5 | nextrtc.join_only_to_existing=false 6 | nextrtc.default_conversation_type=MESH -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/SignalHandler.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.InternalMessage; 4 | 5 | public interface SignalHandler { 6 | void execute(InternalMessage message); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/NextRTCHandler.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 4 | 5 | public interface NextRTCHandler { 6 | 7 | void handleEvent(NextRTCEvent event); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/ConversationFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | 5 | public interface ConversationFactory { 6 | Conversation create(String conversationName, String type); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Connection.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import java.io.IOException; 4 | 5 | public interface Connection { 6 | String getId(); 7 | 8 | boolean isOpen(); 9 | 10 | void sendObject(Object object); 11 | 12 | void close() throws IOException; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings 5 | 6 | *.iml 7 | *.class 8 | 9 | # Mobile Tools for Java (J2ME) 10 | .mtj.tmp/ 11 | 12 | # Package Files # 13 | *.jar 14 | *.war 15 | *.ear 16 | 17 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 18 | hs_err_pid* 19 | /target 20 | 21 | .idea/ 22 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/dto/NextRTCMember.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api.dto; 2 | 3 | 4 | import org.nextrtc.signalingserver.domain.Connection; 5 | 6 | public interface NextRTCMember { 7 | default String getId() { 8 | return getConnection().getId(); 9 | } 10 | 11 | Connection getConnection(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/dto/NextRTCConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api.dto; 2 | 3 | import java.io.Closeable; 4 | import java.util.Set; 5 | 6 | public interface NextRTCConversation extends Closeable { 7 | String getId(); 8 | 9 | NextRTCMember getCreator(); 10 | 11 | Set getMembers(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/ConnectionContextFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | 4 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | 7 | public interface ConnectionContextFactory { 8 | ConnectionContext create(Member from, Member to); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/property/NextRTCProperties.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.property; 2 | 3 | public interface NextRTCProperties { 4 | int getMaxConnectionSetupTime(); 5 | 6 | int getPingPeriod(); 7 | 8 | int getSchedulerPoolSize(); 9 | 10 | boolean isJoinOnlyToExisting(); 11 | 12 | String getDefaultConversationType(); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/performance/Tuple.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.performance; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.eclipse.jetty.websocket.client.WebSocketClient; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public class Tuple { 10 | private WebSocketClient client; 11 | private T socket; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG,stdout 2 | log4j.logger.org.nextrtc=DEBUG 3 | log4j.logger.org.springframework=WARN 4 | log4j.additivity.notRootLogger=false 5 | 6 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/performance/README.md: -------------------------------------------------------------------------------- 1 | to run integration tests 2 | 1. You have to run signaling server (with spring or without spring) 3 | 2. Server **MUST** be run without ssl (http) on port **8080** 4 | 3. Application **MUST** be deployed under ROOT with `/signaling` as a websocket endpoint (`ws://localhost:8080/signaling`) 5 | 3. Then run: `mvn clean test -Dtest-groups=integration` -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/MemberFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.domain.Connection; 4 | import org.nextrtc.signalingserver.domain.Member; 5 | 6 | import java.util.concurrent.ScheduledFuture; 7 | 8 | public interface MemberFactory { 9 | Member create(Connection connection, ScheduledFuture ping); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/eventbus/EventDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import com.google.common.eventbus.AllowConcurrentEvents; 4 | import com.google.common.eventbus.Subscribe; 5 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 6 | 7 | public interface EventDispatcher { 8 | @Subscribe 9 | @AllowConcurrentEvents 10 | void handle(NextRTCEvent event); 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/LocalStreamCreated2.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.nextrtc.signalingserver.EventChecker; 4 | import org.nextrtc.signalingserver.api.NextRTCEvents; 5 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 6 | 7 | @NextRTCEventListener({NextRTCEvents.MEDIA_LOCAL_STREAM_CREATED}) 8 | public class LocalStreamCreated2 extends EventChecker { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/SignalResolver.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import org.nextrtc.signalingserver.cases.SignalHandler; 5 | 6 | import java.util.Optional; 7 | 8 | public interface SignalResolver { 9 | Pair resolve(String string); 10 | 11 | Optional> addCustomSignal(Signal signal, SignalHandler handler); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/ConfigurationBuilder.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nextrtc.signalingserver.DaggerNextRTCComponent; 5 | 6 | @Slf4j 7 | public class ConfigurationBuilder { 8 | 9 | public EndpointConfiguration createDefaultEndpoint() { 10 | log.info("Creating default configuration...."); 11 | return new EndpointConfiguration(DaggerNextRTCComponent.builder().build()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/OfferResponseHandlerTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | 7 | public class OfferResponseHandlerTest extends BaseTest { 8 | 9 | @Autowired 10 | private OfferResponseHandler offerResponseHandler; 11 | 12 | @Test 13 | public void execute() throws Exception { 14 | 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/resolver/ManualSignalResolver.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.resolver; 2 | 3 | import org.nextrtc.signalingserver.cases.SignalHandler; 4 | 5 | import javax.inject.Inject; 6 | import java.util.Map; 7 | 8 | public class ManualSignalResolver extends AbstractSignalResolver { 9 | 10 | @Inject 11 | public ManualSignalResolver(Map handlers) { 12 | super(handlers); 13 | initByDefault(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/repository/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import org.nextrtc.signalingserver.domain.Member; 4 | 5 | import java.io.Closeable; 6 | import java.util.Collection; 7 | import java.util.Optional; 8 | 9 | public interface MemberRepository extends Closeable{ 10 | Collection getAllIds(); 11 | 12 | Optional findBy(String id); 13 | 14 | Member register(Member member); 15 | 16 | void unregister(String id); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/ServerEventCheck.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.nextrtc.signalingserver.EventChecker; 4 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 5 | 6 | import static org.nextrtc.signalingserver.api.NextRTCEvents.CONVERSATION_CREATED; 7 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_OPENED; 8 | 9 | @NextRTCEventListener({SESSION_OPENED, CONVERSATION_CREATED}) 10 | public class ServerEventCheck extends EventChecker { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/annotation/NextRTCEventListener.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api.annotation; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEvents; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.TYPE; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | @Retention(RUNTIME) 12 | @Target({TYPE}) 13 | public @interface NextRTCEventListener { 14 | 15 | NextRTCEvents[] value() default {}; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/CandidateHandler.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Signals; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component(Signals.CANDIDATE_HANDLER) 9 | public class CandidateHandler extends Exchange { 10 | 11 | @Override 12 | protected void exchange(InternalMessage message, Conversation conversation) { 13 | conversation.exchangeSignals(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/OfferResponseHandler.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Signals; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component(Signals.OFFER_RESPONSE_HANDLER) 9 | public class OfferResponseHandler extends Exchange { 10 | 11 | @Override 12 | protected void exchange(InternalMessage message, Conversation conversation) { 13 | conversation.exchangeSignals(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/AnswerResponseHandler.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Signals; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component(Signals.ANSWER_RESPONSE_HANDLER) 9 | public class AnswerResponseHandler extends Exchange { 10 | 11 | @Override 12 | protected void exchange(InternalMessage message, Conversation conversation) { 13 | conversation.exchangeSignals(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/Names.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | public interface Names { 4 | String EVENT_BUS = "nextRTCEventBus"; 5 | String EVENT_DISPATCHER = "nextRTCEventDispatcher"; 6 | 7 | String SCHEDULER_SIZE = "${nextrtc.scheduler_size:10}"; 8 | String SCHEDULER_NAME = "nextRTCPingScheduler"; 9 | String SCHEDULED_PERIOD = "${nextrtc.ping_period:3}"; 10 | String MAX_CONNECTION_SETUP_TIME = "${nextrtc.max_connection_setup_time:30}"; 11 | String JOIN_ONLY_TO_EXISTING = "${nextrtc.join_only_to_existing:false}"; 12 | 13 | String DEFAULT_CONVERSATION_TYPE = "${nextrtc.default_conversation_type:MESH}"; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/repository/ConversationRepository.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.Member; 5 | 6 | import java.io.Closeable; 7 | import java.util.Collection; 8 | import java.util.Optional; 9 | 10 | 11 | public interface ConversationRepository extends Closeable { 12 | Optional findBy(String id); 13 | 14 | Optional findBy(Member from); 15 | 16 | Conversation remove(String id); 17 | 18 | Conversation save(Conversation conversation); 19 | 20 | Collection getAllIds(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/PingTask.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | public class PingTask implements Runnable { 4 | 5 | private MessageSender sender; 6 | private Member to; 7 | 8 | public PingTask(Connection to, MessageSender sender) { 9 | this.to = new Member(to, null); 10 | this.sender = sender; 11 | } 12 | 13 | @Override 14 | public void run() { 15 | if(Thread.interrupted()){ 16 | return; 17 | } 18 | sender.send(InternalMessage.create()// 19 | .to(to)// 20 | .signal(Signal.PING)// 21 | .build()// 22 | ); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/dto/NextRTCEvent.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api.dto; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEvents; 4 | import org.nextrtc.signalingserver.exception.SignalingException; 5 | 6 | import java.time.ZonedDateTime; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | public interface NextRTCEvent { 11 | 12 | NextRTCEvents type(); 13 | 14 | ZonedDateTime published(); 15 | 16 | Optional from(); 17 | 18 | Optional to(); 19 | 20 | Optional conversation(); 21 | 22 | Optional exception(); 23 | 24 | Map custom(); 25 | 26 | String content(); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/property/ManualNextRTCProperties.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.property; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class ManualNextRTCProperties implements NextRTCProperties { 13 | @Builder.Default 14 | private int maxConnectionSetupTime = 30; 15 | @Builder.Default 16 | private int pingPeriod = 3; 17 | @Builder.Default 18 | private int schedulerPoolSize = 10; 19 | @Builder.Default 20 | private boolean joinOnlyToExisting = false; 21 | @Builder.Default 22 | private String defaultConversationType = "MESH"; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/exception/Exceptions.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.exception; 2 | 3 | public enum Exceptions { 4 | MEMBER_NOT_FOUND, 5 | INVALID_RECIPIENT, 6 | MEMBER_IN_OTHER_CONVERSATION, 7 | 8 | INVALID_CONVERSATION_NAME, 9 | CONVERSATION_NAME_OCCUPIED, 10 | CONVERSATION_NOT_FOUND, 11 | 12 | UNKNOWN_ERROR; 13 | 14 | public SignalingException exception() { 15 | return new SignalingException(this); 16 | } 17 | 18 | public SignalingException exception(String customMesage) { 19 | return new SignalingException(this, customMesage); 20 | } 21 | 22 | public SignalingException exception(Exception reason) { 23 | return new SignalingException(this, reason); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/SpringConnectionContextFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 4 | import org.nextrtc.signalingserver.domain.Member; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class SpringConnectionContextFactory implements ConnectionContextFactory { 11 | @Autowired 12 | private ApplicationContext context; 13 | 14 | @Override 15 | public ConnectionContext create(Member from, Member to) { 16 | return context.getBean(ConnectionContext.class, from, to); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/SpringMemberFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.domain.Connection; 4 | import org.nextrtc.signalingserver.domain.Member; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.ApplicationContext; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.concurrent.ScheduledFuture; 10 | 11 | @Component 12 | public class SpringMemberFactory implements MemberFactory { 13 | 14 | @Autowired 15 | private ApplicationContext context; 16 | 17 | @Override 18 | public Member create(Connection connection, ScheduledFuture ping) { 19 | return context.getBean(Member.class, connection, ping); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/property/SpringNextRTCProperties.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.property; 2 | 3 | import lombok.Getter; 4 | import org.nextrtc.signalingserver.Names; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Getter 9 | @Component 10 | public class SpringNextRTCProperties implements NextRTCProperties { 11 | 12 | @Value(Names.MAX_CONNECTION_SETUP_TIME) 13 | private int maxConnectionSetupTime; 14 | 15 | @Value(Names.SCHEDULED_PERIOD) 16 | private int pingPeriod; 17 | 18 | @Value(Names.SCHEDULER_SIZE) 19 | private int schedulerPoolSize; 20 | 21 | @Value(Names.JOIN_ONLY_TO_EXISTING) 22 | private boolean joinOnlyToExisting; 23 | @Value(Names.DEFAULT_CONVERSATION_TYPE) 24 | private String defaultConversationType; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/ManualMemberFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.domain.Connection; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | 7 | import javax.inject.Inject; 8 | import java.util.concurrent.ScheduledFuture; 9 | 10 | public class ManualMemberFactory implements MemberFactory { 11 | 12 | private NextRTCEventBus eventBus; 13 | 14 | @Inject 15 | public ManualMemberFactory(NextRTCEventBus eventBus) { 16 | this.eventBus = eventBus; 17 | } 18 | 19 | @Override 20 | public Member create(Connection connection, ScheduledFuture ping) { 21 | Member member = new Member(connection, ping); 22 | member.setEventBus(eventBus); 23 | return member; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/resolver/SpringSignalResolver.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.resolver; 2 | 3 | import org.nextrtc.signalingserver.cases.SignalHandler; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Map; 10 | 11 | @Component 12 | @Scope("singleton") 13 | public class SpringSignalResolver extends AbstractSignalResolver implements InitializingBean { 14 | 15 | @Autowired 16 | public SpringSignalResolver(Map handlers) { 17 | super(handlers); 18 | } 19 | 20 | @Override 21 | public void afterPropertiesSet() throws Exception { 22 | initByDefault(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/TestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import org.mockito.Answers; 4 | import org.mockito.Mockito; 5 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 6 | import org.springframework.context.annotation.*; 7 | 8 | import java.util.concurrent.ScheduledExecutorService; 9 | 10 | @Configuration 11 | @ComponentScan(basePackages = "org.nextrtc.signalingserver") 12 | @PropertySource("classpath:nextrtc.properties") 13 | public class TestConfig { 14 | 15 | @Primary 16 | @Bean(name = Names.EVENT_BUS) 17 | public NextRTCEventBus eventBus() { 18 | return new NextRTCEventBus(); 19 | } 20 | 21 | @Primary 22 | @Bean(name = Names.SCHEDULER_NAME) 23 | public ScheduledExecutorService scheduler() { 24 | return Mockito.mock(ScheduledExecutorService.class, Answers.RETURNS_DEEP_STUBS.get()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/conversation/MeshConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.conversation; 2 | 3 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 4 | import org.nextrtc.signalingserver.cases.LeftConversation; 5 | import org.nextrtc.signalingserver.domain.MessageSender; 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @Scope("prototype") 11 | public class MeshConversation extends AbstractMeshConversation { 12 | public MeshConversation(String id) { 13 | super(id); 14 | } 15 | 16 | public MeshConversation(String id, LeftConversation left, MessageSender sender, ExchangeSignalsBetweenMembers exchange) { 17 | super(id, left, sender, exchange); 18 | } 19 | 20 | @Override 21 | public String getTypeName() { 22 | return "MESH"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/EventChecker.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import com.google.common.collect.Lists; 4 | import org.nextrtc.signalingserver.api.NextRTCHandler; 5 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | 11 | @Component 12 | @Scope("prototype") 13 | public class EventChecker implements NextRTCHandler { 14 | 15 | List events = Lists.newArrayList(); 16 | 17 | @Override 18 | public void handleEvent(NextRTCEvent event) { 19 | events.add(event); 20 | } 21 | 22 | public void reset() { 23 | events.clear(); 24 | } 25 | 26 | public NextRTCEvent get(int index) { 27 | return events.get(index); 28 | } 29 | 30 | public List getEvents() { 31 | return this.events; 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/Exchange.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | 7 | import static org.nextrtc.signalingserver.exception.Exceptions.INVALID_RECIPIENT; 8 | 9 | public abstract class Exchange implements SignalHandler { 10 | 11 | @Override 12 | public final void execute(InternalMessage message) { 13 | Conversation conversation = checkPrecondition(message.getFrom()); 14 | exchange(message, conversation); 15 | } 16 | 17 | protected abstract void exchange(InternalMessage message, Conversation conversation); 18 | 19 | private Conversation checkPrecondition(Member from) { 20 | if (!from.getConversation().isPresent()) { 21 | throw INVALID_RECIPIENT.exception(); 22 | } 23 | return from.getConversation().get(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/modules/NextRTCRepositories.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.modules; 2 | 3 | import dagger.Binds; 4 | import dagger.Module; 5 | import dagger.Provides; 6 | import org.nextrtc.signalingserver.repository.ConversationRepository; 7 | import org.nextrtc.signalingserver.repository.Conversations; 8 | import org.nextrtc.signalingserver.repository.MemberRepository; 9 | import org.nextrtc.signalingserver.repository.Members; 10 | 11 | import javax.inject.Singleton; 12 | 13 | @Module 14 | public abstract class NextRTCRepositories { 15 | 16 | @Provides 17 | @Singleton 18 | static Members Members() { 19 | return new Members(); 20 | } 21 | 22 | @Provides 23 | @Singleton 24 | static Conversations Conversations() { 25 | return new Conversations(); 26 | } 27 | 28 | @Binds 29 | abstract ConversationRepository conversationRepository(Conversations conversations); 30 | 31 | @Binds 32 | abstract MemberRepository memberRepository(Members members); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/eventbus/EventBusSetup.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import org.nextrtc.signalingserver.Names; 4 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 5 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.annotation.Scope; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.PostConstruct; 13 | 14 | @Component("nextRTCEventBusSetup") 15 | @Scope("singleton") 16 | public class EventBusSetup { 17 | 18 | @Autowired 19 | @Qualifier(Names.EVENT_BUS) 20 | private NextRTCEventBus eventBus; 21 | 22 | @Autowired 23 | private ApplicationContext context; 24 | 25 | @PostConstruct 26 | public void setupHandlers() { 27 | context.getBeansWithAnnotation(NextRTCEventListener.class).values() 28 | .forEach(eventBus::register); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/NextRTCEventBus.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.nextrtc.signalingserver.Names; 6 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 7 | import org.springframework.context.annotation.Scope; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Slf4j 11 | @Service(Names.EVENT_BUS) 12 | @Scope("singleton") 13 | public class NextRTCEventBus { 14 | private EventBus eventBus; 15 | 16 | public NextRTCEventBus() { 17 | this.eventBus = new EventBus(); 18 | } 19 | 20 | public void post(NextRTCEvent event) { 21 | if (event.type() != NextRTCEvents.MESSAGE) { 22 | log.info("POSTED EVENT: " + event); 23 | } 24 | eventBus.post(event); 25 | } 26 | 27 | @Deprecated 28 | public void post(Object o) { 29 | eventBus.post(o); 30 | } 31 | 32 | public void register(Object listeners) { 33 | log.info("REGISTERED LISTENER: " + listeners); 34 | eventBus.register(listeners); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Marcin Ślósarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Message.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.google.gson.annotations.Expose; 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | import java.util.Map; 11 | 12 | import static org.apache.commons.lang3.StringUtils.EMPTY; 13 | 14 | @Getter 15 | @Builder(builderMethodName = "create") 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | public class Message { 18 | /** 19 | * Use Message.create(...) instead of new Message() 20 | */ 21 | @Deprecated 22 | Message() { 23 | } 24 | 25 | @Expose 26 | private String from = EMPTY; 27 | 28 | @Expose 29 | private String to = EMPTY; 30 | 31 | @Expose 32 | private String signal = EMPTY; 33 | 34 | @Expose 35 | private String content = EMPTY; 36 | 37 | @Expose 38 | private Map custom = Maps.newHashMap(); 39 | 40 | @Override 41 | public String toString() { 42 | return String.format("(%s -> %s)[%s]: %s |%s", from, to, signal, content, custom); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Signals.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | public interface Signals { 4 | String EMPTY = ""; 5 | String EMPTY_HANDLER = "nextRTC_EMPTY"; 6 | String OFFER_REQUEST = "offerRequest"; 7 | String OFFER_RESPONSE = "offerResponse"; 8 | String OFFER_RESPONSE_HANDLER = "nextRTC_OFFER_RESPONSE"; 9 | String ANSWER_REQUEST = "answerRequest"; 10 | String ANSWER_RESPONSE = "answerResponse"; 11 | String ANSWER_RESPONSE_HANDLER = "nextRTC_ANSWER_RESPONSE"; 12 | String FINALIZE = "finalize"; 13 | String CANDIDATE = "candidate"; 14 | String CANDIDATE_HANDLER = "nextRTC_CANDIDATE"; 15 | String PING = "ping"; 16 | String LEFT = "left"; 17 | String LEFT_HANDLER = "nextRTC_LEFT"; 18 | String JOIN = "join"; 19 | String JOIN_HANDLER = "nextRTC_JOIN"; 20 | String CREATE = "create"; 21 | String CREATE_HANDLER = "nextRTC_CREATE"; 22 | String JOINED = "joined"; 23 | String CREATED = "created"; 24 | String TEXT = "text"; 25 | String TEXT_HANDLER = "nextRTC_TEXT"; 26 | String NEW_JOINED = "newJoined"; 27 | String ERROR = "error"; 28 | String END = "end"; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/connection/ConnectionState.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases.connection; 2 | 3 | import org.nextrtc.signalingserver.domain.InternalMessage; 4 | import org.nextrtc.signalingserver.domain.Signal; 5 | 6 | public enum ConnectionState { 7 | OFFER_REQUESTED { 8 | @Override 9 | public boolean isValid(InternalMessage message) { 10 | return Signal.OFFER_RESPONSE.is(message.getSignal()); 11 | } 12 | }, 13 | ANSWER_REQUESTED { 14 | @Override 15 | public boolean isValid(InternalMessage message) { 16 | return Signal.ANSWER_RESPONSE.is(message.getSignal()); 17 | } 18 | }, 19 | EXCHANGE_CANDIDATES { 20 | @Override 21 | public boolean isValid(InternalMessage message) { 22 | return Signal.CANDIDATE.is(message.getSignal()); 23 | } 24 | }, 25 | NOT_INITIALIZED { 26 | @Override 27 | public boolean isValid(InternalMessage message) { 28 | return false; 29 | } 30 | }; 31 | 32 | 33 | ConnectionState() { 34 | } 35 | 36 | public abstract boolean isValid(InternalMessage message); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/exception/SignalingException.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.exception; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import static java.lang.String.format; 6 | 7 | public class SignalingException extends RuntimeException { 8 | 9 | private static final long serialVersionUID = 4171073365651049929L; 10 | 11 | private String customMessage; 12 | 13 | public SignalingException(Exceptions exception) { 14 | super(exception.name()); 15 | } 16 | 17 | public SignalingException(Exceptions exception, Throwable t) { 18 | super(exception.name(), t); 19 | } 20 | 21 | public SignalingException(Exceptions exception, String customMessage) { 22 | super(exception.name()); 23 | this.customMessage = customMessage; 24 | } 25 | 26 | 27 | public String getCustomMessage() { 28 | return StringUtils.defaultString(customMessage); 29 | } 30 | 31 | public void throwException() { 32 | throw this; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return format("Signaling Exception %s [%s]", getMessage(), getCustomMessage()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/TextMessage.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.domain.*; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.inject.Inject; 8 | 9 | import static org.nextrtc.signalingserver.api.NextRTCEvents.TEXT; 10 | 11 | @Component(Signals.TEXT_HANDLER) 12 | public class TextMessage implements SignalHandler { 13 | 14 | private NextRTCEventBus eventBus; 15 | private MessageSender sender; 16 | 17 | @Inject 18 | public TextMessage(NextRTCEventBus eventBus, 19 | MessageSender sender) { 20 | this.eventBus = eventBus; 21 | this.sender = sender; 22 | } 23 | 24 | @Override 25 | public void execute(InternalMessage message) { 26 | Member from = message.getFrom(); 27 | if (message.getTo() == null && from.getConversation().isPresent()) { 28 | Conversation conversation = from.getConversation().get(); 29 | conversation.broadcast(from, message); 30 | eventBus.post(TEXT.basedOn(message)); 31 | } else if (from.hasSameConversation(message.getTo())) { 32 | sender.send(message); 33 | eventBus.post(TEXT.basedOn(message)); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/EndpointConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.nextrtc.signalingserver.NextRTCComponent; 6 | import org.nextrtc.signalingserver.domain.MessageSender; 7 | import org.nextrtc.signalingserver.domain.Server; 8 | import org.nextrtc.signalingserver.domain.resolver.ManualSignalResolver; 9 | import org.nextrtc.signalingserver.eventbus.ManualEventDispatcher; 10 | import org.nextrtc.signalingserver.property.ManualNextRTCProperties; 11 | 12 | @Slf4j 13 | public class EndpointConfiguration { 14 | private final NextRTCComponent component; 15 | 16 | public EndpointConfiguration(NextRTCComponent component) { 17 | this.component = component; 18 | } 19 | 20 | public Server nextRTCServer() { 21 | return component.nextRTCServer(); 22 | } 23 | 24 | public ManualNextRTCProperties nextRTCProperties() { 25 | return component.manualProperties(); 26 | } 27 | 28 | public ManualSignalResolver signalResolver() { 29 | return component.manualSignalResolver(); 30 | } 31 | 32 | public ManualEventDispatcher eventDispatcher() { 33 | return component.manualEventDispatcher(); 34 | } 35 | 36 | public MessageSender messageSender() { 37 | return component.messageSender(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/modules/NextRTCMedia.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.modules; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 6 | import org.nextrtc.signalingserver.domain.DefaultMessageSender; 7 | import org.nextrtc.signalingserver.domain.RTCConnections; 8 | import org.nextrtc.signalingserver.factory.ConnectionContextFactory; 9 | import org.nextrtc.signalingserver.property.NextRTCProperties; 10 | import org.nextrtc.signalingserver.repository.MemberRepository; 11 | 12 | import javax.inject.Singleton; 13 | import java.util.concurrent.ScheduledExecutorService; 14 | 15 | @Module 16 | public abstract class NextRTCMedia { 17 | 18 | @Provides 19 | static DefaultMessageSender defaultMessageSender(MemberRepository repo) { 20 | return new DefaultMessageSender(repo); 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | static RTCConnections RTCConnections(ScheduledExecutorService scheduler, NextRTCProperties properties) { 26 | return new RTCConnections(scheduler, properties); 27 | } 28 | 29 | @Provides 30 | static ExchangeSignalsBetweenMembers ExchangeSignalsBetweenMembers(RTCConnections connections, ConnectionContextFactory factory) { 31 | return new ExchangeSignalsBetweenMembers(connections, factory); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/ExchangeSignalsBetweenMembers.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | import org.nextrtc.signalingserver.domain.RTCConnections; 7 | import org.nextrtc.signalingserver.factory.ConnectionContextFactory; 8 | import org.springframework.context.annotation.Scope; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | 13 | @Component 14 | @Scope("prototype") 15 | public class ExchangeSignalsBetweenMembers { 16 | 17 | private RTCConnections connections; 18 | private ConnectionContextFactory factory; 19 | 20 | @Inject 21 | public ExchangeSignalsBetweenMembers(RTCConnections connections, ConnectionContextFactory factory) { 22 | this.connections = connections; 23 | this.factory = factory; 24 | } 25 | 26 | public synchronized void begin(Member from, Member to) { 27 | connections.put(from, to, factory.create(from, to)); 28 | connections.get(from, to).ifPresent(ConnectionContext::begin); 29 | } 30 | 31 | public synchronized void execute(InternalMessage message) { 32 | connections.get(message.getFrom(), message.getTo()).ifPresent(context -> context.process(message)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/ManualConnectionContextFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | import org.nextrtc.signalingserver.domain.MessageSender; 7 | import org.nextrtc.signalingserver.property.NextRTCProperties; 8 | 9 | import javax.inject.Inject; 10 | 11 | public class ManualConnectionContextFactory implements ConnectionContextFactory { 12 | 13 | private NextRTCProperties properties; 14 | private NextRTCEventBus eventBus; 15 | private MessageSender sender; 16 | 17 | @Inject 18 | public ManualConnectionContextFactory(NextRTCProperties properties, 19 | NextRTCEventBus eventBus, 20 | MessageSender sender) { 21 | this.properties = properties; 22 | this.eventBus = eventBus; 23 | this.sender = sender; 24 | 25 | } 26 | 27 | @Override 28 | public ConnectionContext create(Member from, Member to) { 29 | ConnectionContext connectionContext = new ConnectionContext(from, to); 30 | connectionContext.setBus(eventBus); 31 | connectionContext.setProperties(properties); 32 | connectionContext.setSender(sender); 33 | return connectionContext; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/NextRTCComponent.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import dagger.Component; 4 | import org.nextrtc.signalingserver.domain.MessageSender; 5 | import org.nextrtc.signalingserver.domain.Server; 6 | import org.nextrtc.signalingserver.domain.resolver.ManualSignalResolver; 7 | import org.nextrtc.signalingserver.eventbus.ManualEventDispatcher; 8 | import org.nextrtc.signalingserver.factory.ManualConversationFactory; 9 | import org.nextrtc.signalingserver.modules.*; 10 | import org.nextrtc.signalingserver.property.ManualNextRTCProperties; 11 | import org.nextrtc.signalingserver.repository.ConversationRepository; 12 | import org.nextrtc.signalingserver.repository.MemberRepository; 13 | 14 | import javax.inject.Singleton; 15 | 16 | @Singleton 17 | @Component(modules = {NextRTCBeans.class, 18 | NextRTCSignals.class, 19 | NextRTCRepositories.class, 20 | NextRTCFactories.class, 21 | NextRTCMedia.class}) 22 | public interface NextRTCComponent { 23 | 24 | ManualNextRTCProperties manualProperties(); 25 | 26 | ManualEventDispatcher manualEventDispatcher(); 27 | 28 | ManualSignalResolver manualSignalResolver(); 29 | 30 | ManualConversationFactory manualConversationFactory(); 31 | 32 | MessageSender messageSender(); 33 | 34 | MemberRepository memberRepository(); 35 | 36 | ConversationRepository conversationRepository(); 37 | 38 | Server nextRTCServer(); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/resolver/SpringSignalResolverTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.resolver; 2 | 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import org.junit.Test; 5 | import org.nextrtc.signalingserver.BaseTest; 6 | import org.nextrtc.signalingserver.cases.SignalHandler; 7 | import org.nextrtc.signalingserver.domain.Signal; 8 | import org.nextrtc.signalingserver.domain.Signals; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import static org.hamcrest.Matchers.is; 12 | import static org.junit.Assert.assertNotNull; 13 | import static org.junit.Assert.assertThat; 14 | 15 | public class SpringSignalResolverTest extends BaseTest { 16 | 17 | @Autowired 18 | private SpringSignalResolver signals; 19 | 20 | @Test 21 | public void shouldCheckResolvingSignalBasedOnString() throws Exception { 22 | // given 23 | 24 | // when 25 | Pair existing = signals.resolve(Signals.FINALIZE); 26 | 27 | // then 28 | assertNotNull(existing); 29 | assertThat(existing.getKey(), is(Signal.FINALIZE)); 30 | } 31 | 32 | @Test 33 | public void shouldReturnDefaultImplementationOnNotExistingSignal() throws Exception { 34 | // given 35 | 36 | // when 37 | Pair notExisting = signals.resolve("not existing"); 38 | 39 | // then 40 | assertNotNull(notExisting); 41 | assertThat(notExisting.getKey(), is(Signal.EMPTY)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/PingTaskTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.MessageMatcher; 6 | import org.nextrtc.signalingserver.repository.MemberRepository; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import static org.hamcrest.Matchers.hasSize; 10 | import static org.hamcrest.Matchers.is; 11 | import static org.junit.Assert.assertThat; 12 | import static org.mockito.Mockito.when; 13 | 14 | public class PingTaskTest extends BaseTest { 15 | 16 | @Autowired 17 | private MessageSender sender; 18 | 19 | @Autowired 20 | private MemberRepository members; 21 | 22 | @Test 23 | public void shouldSendMessageWhenSessionIsOpen() { 24 | // given 25 | MessageMatcher messages = new MessageMatcher(""); 26 | Member member = mockMember("s1", messages); 27 | members.register(member); 28 | 29 | 30 | // when 31 | new PingTask(member.getConnection(), sender).run(); 32 | 33 | // then 34 | assertThat(messages.getMessage().getSignal(), is(Signals.PING)); 35 | } 36 | 37 | @Test 38 | public void shouldNotSendMessageWhenSessionIsEnded() { 39 | // given 40 | MessageMatcher messages = new MessageMatcher(""); 41 | Connection connection = mockConnection("s1", messages); 42 | when(connection.isOpen()).thenReturn(false); 43 | 44 | // when 45 | new PingTask(connection, sender).run(); 46 | 47 | // then 48 | assertThat(messages.getMessages(), hasSize(0)); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/AbstractConversationFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.property.NextRTCProperties; 5 | 6 | import java.util.Map; 7 | import java.util.UUID; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.function.Function; 10 | 11 | import static org.apache.commons.lang3.StringUtils.defaultString; 12 | import static org.apache.commons.lang3.StringUtils.isBlank; 13 | 14 | public abstract class AbstractConversationFactory implements ConversationFactory { 15 | private final Map> supportedTypes = new ConcurrentHashMap<>(); 16 | private final NextRTCProperties properties; 17 | 18 | AbstractConversationFactory(NextRTCProperties properties){ 19 | this.properties = properties; 20 | } 21 | 22 | @Override 23 | public final Conversation create(String id, String optionalType) { 24 | String conversationName = getConversationName(id); 25 | if(supportedTypes.containsKey(defaultString(optionalType))){ 26 | return supportedTypes.get(optionalType).apply(conversationName); 27 | } 28 | return supportedTypes.get(properties.getDefaultConversationType()).apply(conversationName); 29 | } 30 | 31 | public Function registerConversationType(String type, Function creator){ 32 | return supportedTypes.put(type, creator); 33 | } 34 | 35 | private String getConversationName(String name) { 36 | return isBlank(name) ? UUID.randomUUID().toString() : name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/conversation/MeshWithMasterConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.conversation; 2 | 3 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 4 | import org.nextrtc.signalingserver.cases.LeftConversation; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | import org.nextrtc.signalingserver.domain.MessageSender; 7 | import org.springframework.context.annotation.Scope; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.HashSet; 11 | 12 | @Component 13 | @Scope("prototype") 14 | public class MeshWithMasterConversation extends AbstractMeshConversation { 15 | private Member owner; 16 | 17 | public MeshWithMasterConversation(String id) { 18 | super(id); 19 | } 20 | 21 | public MeshWithMasterConversation(String id, LeftConversation left, MessageSender sender, ExchangeSignalsBetweenMembers exchange) { 22 | super(id, left, sender, exchange); 23 | } 24 | 25 | @Override 26 | public String getTypeName() { 27 | return "MESH_WITH_MASTER"; 28 | } 29 | 30 | @Override 31 | public synchronized void join(Member sender) { 32 | if(isWithoutMember()){ 33 | owner = sender; 34 | } 35 | super.join(sender); 36 | } 37 | 38 | @Override 39 | public synchronized boolean remove(Member leaving) { 40 | boolean remove = super.remove(leaving); 41 | if(remove && owner.equals(leaving)){ 42 | new HashSet<>(members()).forEach(super::remove); 43 | } 44 | return remove; 45 | } 46 | 47 | @Override 48 | public Member getCreator() { 49 | return owner; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/CloseableContext.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nextrtc.signalingserver.repository.ConversationRepository; 5 | import org.nextrtc.signalingserver.repository.MemberRepository; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.inject.Inject; 9 | import java.io.Closeable; 10 | import java.io.IOException; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | 13 | @Slf4j 14 | @Component 15 | class CloseableContext implements Closeable { 16 | private final ScheduledExecutorService scheduler; 17 | private final RTCConnections connections; 18 | private final MemberRepository members; 19 | private final ConversationRepository conversations; 20 | 21 | @Inject 22 | public CloseableContext(ScheduledExecutorService scheduler, 23 | RTCConnections connections, 24 | MemberRepository members, 25 | ConversationRepository conversations) { 26 | this.scheduler = scheduler; 27 | this.connections = connections; 28 | this.members = members; 29 | this.conversations = conversations; 30 | } 31 | 32 | @Override 33 | public void close() throws IOException { 34 | scheduler.shutdownNow(); 35 | closeQuietly(conversations); 36 | closeQuietly(members); 37 | closeQuietly(connections); 38 | } 39 | 40 | private void closeQuietly(Closeable closeable) { 41 | try { 42 | closeable.close(); 43 | } catch (Exception e) { 44 | log.error("Problem during closing " + closeable, e); 45 | } 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/SpringConversationFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.conversation.BroadcastConversation; 5 | import org.nextrtc.signalingserver.domain.conversation.MeshConversation; 6 | import org.nextrtc.signalingserver.domain.conversation.MeshWithMasterConversation; 7 | import org.nextrtc.signalingserver.property.NextRTCProperties; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationContext; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class SpringConversationFactory extends AbstractConversationFactory { 14 | private ApplicationContext context; 15 | 16 | @Autowired 17 | public SpringConversationFactory(ApplicationContext context, NextRTCProperties properties) { 18 | super(properties); 19 | this.context = context; 20 | registerConversationType("MESH", this::createMesh); 21 | registerConversationType("MESH_WITH_MASTER", this::createMeshWithMaster); 22 | registerConversationType("BROADCAST", this::createBroadcast); 23 | } 24 | 25 | private Conversation createMesh(String conversationName) { 26 | return context.getBean(MeshConversation.class, conversationName); 27 | } 28 | 29 | private Conversation createMeshWithMaster(String conversationName) { 30 | return context.getBean(MeshWithMasterConversation.class, conversationName); 31 | } 32 | 33 | private Conversation createBroadcast(String conversationName) { 34 | return context.getBean(BroadcastConversation.class, conversationName); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/ExchangeSignalsBetweenMembersTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 6 | import org.nextrtc.signalingserver.cases.connection.ConnectionState; 7 | import org.nextrtc.signalingserver.domain.Member; 8 | import org.nextrtc.signalingserver.domain.RTCConnections; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.hamcrest.Matchers.is; 14 | import static org.junit.Assert.assertThat; 15 | 16 | 17 | public class ExchangeSignalsBetweenMembersTest extends BaseTest { 18 | 19 | @Autowired 20 | private ExchangeSignalsBetweenMembers exchange1; 21 | 22 | @Autowired 23 | private ExchangeSignalsBetweenMembers exchange2; 24 | 25 | @Autowired 26 | private RTCConnections connections; 27 | 28 | @Test 29 | public void begin() throws Exception { 30 | // given 31 | Member john = mockMember("john"); 32 | Member stan = mockMember("stan"); 33 | Member ed = mockMember("ed"); 34 | 35 | // when 36 | 37 | exchange1.begin(john, stan); 38 | exchange2.begin(john, ed); 39 | 40 | // then 41 | Optional first = connections.get(john, stan); 42 | assertThat(first.isPresent(), is(true)); 43 | assertThat(first.get().getState(), is(ConnectionState.OFFER_REQUESTED)); 44 | Optional second = connections.get(ed, john); 45 | assertThat(second.isPresent(), is(true)); 46 | assertThat(second.get().getState(), is(ConnectionState.OFFER_REQUESTED)); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/eventbus/SpringEventDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nextrtc.signalingserver.Names; 5 | import org.nextrtc.signalingserver.api.NextRTCEvents; 6 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 7 | import org.springframework.aop.framework.Advised; 8 | import org.springframework.aop.support.AopUtils; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Scope; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.Collection; 15 | import java.util.Map; 16 | 17 | import static org.springframework.core.annotation.AnnotationUtils.getValue; 18 | 19 | @Slf4j 20 | @Component(Names.EVENT_DISPATCHER) 21 | @Scope("singleton") 22 | @NextRTCEventListener 23 | public class SpringEventDispatcher extends AbstractEventDispatcher { 24 | @Autowired 25 | private ApplicationContext context; 26 | 27 | protected Collection getNextRTCEventListeners() { 28 | Map beans = context.getBeansWithAnnotation(NextRTCEventListener.class); 29 | beans.remove(Names.EVENT_DISPATCHER); 30 | return beans.values(); 31 | } 32 | 33 | @Override 34 | protected NextRTCEvents[] getSupportedEvents(Object listener) { 35 | try { 36 | if (AopUtils.isJdkDynamicProxy(listener)) { 37 | listener = ((Advised) listener).getTargetSource().getTarget(); 38 | } 39 | } catch (Exception e) { 40 | return new NextRTCEvents[0]; 41 | } 42 | return (NextRTCEvents[]) getValue(listener.getClass().getAnnotation(NextRTCEventListener.class)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/repository/Members.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import com.google.common.collect.Maps; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.io.IOException; 9 | import java.util.Collection; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | @Slf4j 14 | @Repository 15 | public class Members implements MemberRepository { 16 | 17 | private Map members = Maps.newConcurrentMap(); 18 | 19 | @Override 20 | public Collection getAllIds() { 21 | return members.keySet(); 22 | } 23 | 24 | @Override 25 | public Optional findBy(String id) { 26 | if (id == null) { 27 | return Optional.empty(); 28 | } 29 | return Optional.ofNullable(members.get(id)); 30 | } 31 | 32 | @Override 33 | public Member register(Member member) { 34 | if (!members.containsKey(member.getId())) { 35 | members.put(member.getId(), member); 36 | } 37 | return member; 38 | } 39 | 40 | @Override 41 | public void unregister(String id) { 42 | findBy(id).ifPresent(Member::markLeft); 43 | Member removed = members.remove(id); 44 | if (removed != null) { 45 | removed.getConversation().ifPresent(c -> c.left(removed)); 46 | } 47 | } 48 | 49 | @Override 50 | public void close() throws IOException { 51 | for (Member member : members.values()) { 52 | try { 53 | member.getConnection().close(); 54 | } catch (Exception e) { 55 | log.error("Problem during closing member connection " + member.getId(), e); 56 | } 57 | } 58 | members.clear(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/NextRTCConfig.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.property.NextRTCProperties; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 10 | import org.springframework.core.io.ClassPathResource; 11 | import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; 12 | 13 | import java.util.concurrent.ScheduledExecutorService; 14 | 15 | @Configuration 16 | @ComponentScan(basePackageClasses = {NextRTCConfig.class}) 17 | public class NextRTCConfig { 18 | 19 | @Autowired 20 | private NextRTCProperties properties; 21 | 22 | @Bean(name = Names.EVENT_BUS) 23 | public NextRTCEventBus eventBus() { 24 | return new NextRTCEventBus(); 25 | } 26 | 27 | @Bean 28 | public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { 29 | PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer(); 30 | propertyPlaceholderConfigurer.setLocation(new ClassPathResource("nextrtc.properties")); 31 | return propertyPlaceholderConfigurer; 32 | } 33 | 34 | @Bean(name = Names.SCHEDULER_NAME) 35 | public ScheduledExecutorService scheduler() { 36 | ScheduledExecutorFactoryBean factoryBean = new ScheduledExecutorFactoryBean(); 37 | factoryBean.setThreadNamePrefix("NextRTCConfig"); 38 | factoryBean.setPoolSize(properties.getSchedulerPoolSize()); 39 | factoryBean.afterPropertiesSet(); 40 | return factoryBean.getObject(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/NextRTCServer.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import org.nextrtc.signalingserver.domain.Connection; 6 | import org.nextrtc.signalingserver.domain.Message; 7 | 8 | import java.io.Closeable; 9 | import java.util.function.Function; 10 | 11 | import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4; 12 | 13 | public interface NextRTCServer extends Closeable { 14 | 15 | void register(Connection connection); 16 | 17 | void handle(Message external, Connection connection); 18 | 19 | default void handle(String message, Connection connection) { 20 | Message decode; 21 | try { 22 | decode = MessageDecoder.decode(message); 23 | } catch (Exception e){ 24 | throw new IllegalArgumentException(e); 25 | } 26 | handle(decode, connection); 27 | } 28 | 29 | void unregister(Connection connection, String reason); 30 | 31 | void handleError(Connection connection, Throwable exception); 32 | 33 | 34 | static NextRTCServer create(Function function) { 35 | return function.apply(new ConfigurationBuilder().createDefaultEndpoint()).nextRTCServer(); 36 | } 37 | 38 | class MessageDecoder { 39 | 40 | private static Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); 41 | 42 | public static Message decode(String json) { 43 | return gson.fromJson(escapeHtml4(json.replace("\"", "'")), Message.class); 44 | } 45 | 46 | } 47 | 48 | class MessageEncoder { 49 | 50 | private static Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); 51 | 52 | public static String encode(Object json) { 53 | return gson.toJson(json); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/eventbus/ManualEventDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.api.NextRTCEvents; 5 | import org.nextrtc.signalingserver.api.NextRTCHandler; 6 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 7 | 8 | import java.lang.annotation.Annotation; 9 | import java.lang.reflect.Method; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | @NextRTCEventListener 15 | public class ManualEventDispatcher extends AbstractEventDispatcher { 16 | 17 | private final List events = new ArrayList<>(); 18 | 19 | private final NextRTCEventBus eventBus; 20 | 21 | public ManualEventDispatcher(NextRTCEventBus eventBus) { 22 | this.eventBus = eventBus; 23 | } 24 | 25 | protected Collection getNextRTCEventListeners() { 26 | return events; 27 | } 28 | 29 | @Override 30 | protected NextRTCEvents[] getSupportedEvents(Object listener) { 31 | return (NextRTCEvents[]) getValue(listener.getClass().getAnnotation(NextRTCEventListener.class)); 32 | 33 | } 34 | 35 | public static Object getValue(Annotation annotation) { 36 | if (annotation != null) { 37 | try { 38 | Method method = annotation.annotationType().getDeclaredMethod("value", new Class[0]); 39 | method.setAccessible(true); 40 | return method.invoke(annotation, new Object[0]); 41 | } catch (Exception var3) { 42 | return null; 43 | } 44 | } else { 45 | return null; 46 | } 47 | } 48 | 49 | public void addListener(NextRTCHandler handler) { 50 | if (handler != null) { 51 | eventBus.register(handler); 52 | events.add(handler); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/repository/MembersTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.domain.Connection; 6 | import org.nextrtc.signalingserver.domain.Member; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.hamcrest.Matchers.is; 12 | import static org.junit.Assert.*; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | public class MembersTest extends BaseTest { 17 | 18 | @Autowired 19 | private Members members; 20 | 21 | @Test 22 | public void shouldAddMember() throws Exception { 23 | // given 24 | 25 | // when 26 | members.register(mockMember("s1")); 27 | 28 | // then 29 | assertThat(members.findBy("s1").isPresent(), is(true)); 30 | } 31 | 32 | @Test 33 | public void shouldWorkWhenMemberDoesntExists() throws Exception { 34 | // given 35 | 36 | // when 37 | Optional findBy = members.findBy("not existing one"); 38 | 39 | // then 40 | assertThat(findBy.isPresent(), is(false)); 41 | } 42 | 43 | @Test 44 | public void shouldWorkWhenMemberIsNull() throws Exception { 45 | // given 46 | 47 | // when 48 | Optional findBy = members.findBy(null); 49 | 50 | // then 51 | assertThat(findBy.isPresent(), is(false)); 52 | } 53 | 54 | @Test 55 | public void shouldUnregisterMember() throws Exception { 56 | // given 57 | Connection connection = mock(Connection.class); 58 | when(connection.getId()).thenReturn("s1"); 59 | members.register(mockMember("s1")); 60 | assertTrue(members.findBy("s1").isPresent()); 61 | 62 | // when 63 | members.unregister(connection.getId()); 64 | 65 | // then 66 | assertFalse(members.findBy("s1").isPresent()); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/CreateConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.domain.Conversation; 5 | import org.nextrtc.signalingserver.domain.InternalMessage; 6 | import org.nextrtc.signalingserver.domain.Signals; 7 | import org.nextrtc.signalingserver.exception.SignalingException; 8 | import org.nextrtc.signalingserver.factory.ConversationFactory; 9 | import org.nextrtc.signalingserver.repository.ConversationRepository; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.inject.Inject; 13 | 14 | import static org.nextrtc.signalingserver.api.NextRTCEvents.CONVERSATION_CREATED; 15 | import static org.nextrtc.signalingserver.exception.Exceptions.MEMBER_IN_OTHER_CONVERSATION; 16 | 17 | @Component(Signals.CREATE_HANDLER) 18 | public class CreateConversation implements SignalHandler { 19 | 20 | private NextRTCEventBus eventBus; 21 | private ConversationRepository conversations; 22 | private ConversationFactory factory; 23 | 24 | @Inject 25 | public CreateConversation(NextRTCEventBus eventBus, 26 | ConversationRepository conversations, 27 | ConversationFactory factory) { 28 | this.eventBus = eventBus; 29 | this.conversations = conversations; 30 | this.factory = factory; 31 | } 32 | 33 | 34 | public synchronized void execute(InternalMessage context) { 35 | conversations.findBy(context.getFrom()) 36 | .map(Conversation::getId) 37 | .map(MEMBER_IN_OTHER_CONVERSATION::exception) 38 | .ifPresent(SignalingException::throwException); 39 | 40 | 41 | String id = context.getContent(); 42 | 43 | Conversation conversation = conversations.save(factory.create(id, context.getCustom().get("type"))); 44 | eventBus.post(CONVERSATION_CREATED.basedOn(context, conversation)); 45 | 46 | conversation.join(context.getFrom()); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/LeftConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.domain.Conversation; 5 | import org.nextrtc.signalingserver.domain.InternalMessage; 6 | import org.nextrtc.signalingserver.domain.Member; 7 | import org.nextrtc.signalingserver.domain.Signals; 8 | import org.nextrtc.signalingserver.repository.ConversationRepository; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | import java.util.Optional; 13 | 14 | import static org.nextrtc.signalingserver.api.NextRTCEvents.CONVERSATION_DESTROYED; 15 | import static org.nextrtc.signalingserver.domain.EventContext.builder; 16 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NOT_FOUND; 17 | 18 | @Component(Signals.LEFT_HANDLER) 19 | public class LeftConversation implements SignalHandler { 20 | 21 | private NextRTCEventBus eventBus; 22 | private ConversationRepository conversations; 23 | 24 | @Inject 25 | public LeftConversation(NextRTCEventBus eventBus, ConversationRepository conversations) { 26 | this.eventBus = eventBus; 27 | this.conversations = conversations; 28 | } 29 | 30 | public void execute(InternalMessage context) { 31 | final Member leaving = context.getFrom(); 32 | Conversation conversation = checkPrecondition(leaving.getConversation()); 33 | 34 | conversation.left(leaving); 35 | } 36 | 37 | public void destroy(Conversation toRemove, Member last) { 38 | eventBus.post(CONVERSATION_DESTROYED.basedOn( 39 | builder() 40 | .conversation(conversations.remove(toRemove.getId())) 41 | .from(last))); 42 | } 43 | 44 | private Conversation checkPrecondition(Optional conversation) { 45 | if (!conversation.isPresent()) { 46 | throw CONVERSATION_NOT_FOUND.exception(); 47 | } 48 | return conversation.get(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/eventbus/AbstractEventDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import com.google.common.eventbus.AllowConcurrentEvents; 4 | import com.google.common.eventbus.Subscribe; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.nextrtc.signalingserver.api.NextRTCEvents; 7 | import org.nextrtc.signalingserver.api.NextRTCHandler; 8 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 9 | 10 | import java.util.Collection; 11 | 12 | @Slf4j 13 | public abstract class AbstractEventDispatcher implements EventDispatcher { 14 | @Override 15 | @Subscribe 16 | @AllowConcurrentEvents 17 | public void handle(NextRTCEvent event) { 18 | getNextRTCEventListeners().parallelStream() 19 | .filter(listener -> isNextRTCHandler(listener) && supportsCurrentEvent(listener, event)) 20 | .forEach(listener -> doTry(() -> ((NextRTCHandler) listener).handleEvent(event))); 21 | 22 | } 23 | 24 | protected abstract Collection getNextRTCEventListeners(); 25 | 26 | private void doTry(Runnable action) { 27 | try { 28 | action.run(); 29 | } catch (Exception e) { 30 | log.error("Handler throws an exception", e); 31 | } 32 | } 33 | 34 | private boolean isNextRTCHandler(Object listener) { 35 | return listener instanceof NextRTCHandler; 36 | } 37 | 38 | private boolean supportsCurrentEvent(Object listener, NextRTCEvent event) { 39 | NextRTCEvents[] events = getSupportedEvents(listener); 40 | if (events == null) { 41 | return false; 42 | } 43 | for (NextRTCEvents supportedEvent : events) { 44 | if (isSupporting(event, supportedEvent)) { 45 | return true; 46 | } 47 | } 48 | return false; 49 | } 50 | 51 | private boolean isSupporting(NextRTCEvent msg, NextRTCEvents supportedEvent) { 52 | return supportedEvent.equals(msg.type()); 53 | } 54 | 55 | protected abstract NextRTCEvents[] getSupportedEvents(Object listener); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/modules/NextRTCFactories.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.modules; 2 | 3 | import dagger.Binds; 4 | import dagger.Module; 5 | import dagger.Provides; 6 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 7 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 8 | import org.nextrtc.signalingserver.cases.LeftConversation; 9 | import org.nextrtc.signalingserver.domain.MessageSender; 10 | import org.nextrtc.signalingserver.factory.*; 11 | import org.nextrtc.signalingserver.property.NextRTCProperties; 12 | 13 | import javax.inject.Singleton; 14 | 15 | @Module 16 | public abstract class NextRTCFactories { 17 | 18 | @Provides 19 | @Singleton 20 | static ManualConversationFactory ManualConversationFactory(LeftConversation left, 21 | MessageSender sender, 22 | ExchangeSignalsBetweenMembers exchange, 23 | NextRTCProperties properties) { 24 | return new ManualConversationFactory(left, exchange, sender, properties); 25 | } 26 | 27 | @Provides 28 | @Singleton 29 | static ManualMemberFactory ManualMemberFactory(NextRTCEventBus eventBus) { 30 | return new ManualMemberFactory(eventBus); 31 | } 32 | 33 | @Provides 34 | @Singleton 35 | static ManualConnectionContextFactory ManualConnectionContextFactory( 36 | NextRTCProperties properties, 37 | NextRTCEventBus eventBus, 38 | MessageSender sender) { 39 | return new ManualConnectionContextFactory(properties, eventBus, sender); 40 | } 41 | 42 | @Binds 43 | abstract ConversationFactory ConversationFactory(ManualConversationFactory manualConversationFactory); 44 | 45 | @Binds 46 | abstract MemberFactory MemberFactory(ManualMemberFactory memberFactory); 47 | 48 | @Binds 49 | abstract ConnectionContextFactory ConnectionContextFactory(ManualConnectionContextFactory manualConnectionContextFactory); 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/RTCConnectionsTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import java.util.Optional; 9 | 10 | import static org.hamcrest.core.Is.is; 11 | import static org.junit.Assert.*; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.when; 14 | 15 | 16 | public class RTCConnectionsTest extends BaseTest { 17 | 18 | @Autowired 19 | private RTCConnections connections; 20 | 21 | @Test 22 | public void shouldInitBiDirectionalConnectionBetweenTwoMembers() { 23 | // given 24 | Member john = mockMember("John"); 25 | Member stan = mockMember("Stan"); 26 | ConnectionContext context = mock(ConnectionContext.class); 27 | 28 | // when 29 | connections.put(john, stan, context); 30 | 31 | // then 32 | Optional result = connections.get(john, stan); 33 | assertTrue(result.isPresent()); 34 | Optional reverse = connections.get(stan, john); 35 | assertTrue(reverse.isPresent()); 36 | assertThat(result.get(), is(context)); 37 | assertThat(reverse.get(), is(result.get())); 38 | } 39 | 40 | @Test 41 | public void shouldRemoveOldConnections() throws Exception { 42 | // given 43 | Member john = mockMember("John"); 44 | Member stan = mockMember("Stan"); 45 | ConnectionContext context = mock(ConnectionContext.class); 46 | when(context.getMaster()).thenReturn(john); 47 | when(context.getSlave()).thenReturn(stan); 48 | when(context.isCurrent()).thenReturn(false); 49 | connections.put(john, stan, context); 50 | 51 | // when 52 | connections.removeOldConnections(); 53 | 54 | // then 55 | Optional result = connections.get(john, stan); 56 | assertFalse(result.isPresent()); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/eventbus/EventDispatcherTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.api.NextRTCEvents; 6 | import org.nextrtc.signalingserver.api.NextRTCHandler; 7 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 8 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 9 | import org.nextrtc.signalingserver.domain.EventContext; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.test.context.ContextConfiguration; 13 | 14 | import java.util.List; 15 | 16 | import static org.hamcrest.Matchers.greaterThan; 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | import static org.nextrtc.signalingserver.api.NextRTCEvents.TEXT; 20 | 21 | 22 | @ContextConfiguration(classes = {TextHandler.class, SecondHandler.class}) 23 | public class EventDispatcherTest extends BaseTest { 24 | 25 | @Autowired 26 | private EventDispatcher dispatcher; 27 | 28 | @Autowired 29 | private List handlers; 30 | 31 | @Test 32 | public void shouldHandleAllMessagesEvenIfTheyAreThrowingExceptions() { 33 | // given 34 | assertThat(handlers.size(), greaterThan(1)); 35 | handlers.forEach(h -> h.used = false); 36 | 37 | // when 38 | dispatcher.handle(EventContext.builder() 39 | .type(NextRTCEvents.TEXT) 40 | .build()); 41 | 42 | // then 43 | handlers.forEach(h -> assertThat(h.used, is(true))); 44 | } 45 | } 46 | 47 | @Component("throwingExceptionHandler") 48 | @NextRTCEventListener(TEXT) 49 | class TextHandler implements NextRTCHandler { 50 | 51 | boolean used; 52 | 53 | @Override 54 | public void handleEvent(NextRTCEvent event) { 55 | used = true; 56 | throw new RuntimeException(); 57 | } 58 | } 59 | 60 | @Component("throwingExceptionHandler2") 61 | @NextRTCEventListener(TEXT) 62 | class SecondHandler extends TextHandler { 63 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/factory/ManualConversationFactory.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 4 | import org.nextrtc.signalingserver.cases.LeftConversation; 5 | import org.nextrtc.signalingserver.domain.Conversation; 6 | import org.nextrtc.signalingserver.domain.MessageSender; 7 | import org.nextrtc.signalingserver.domain.conversation.BroadcastConversation; 8 | import org.nextrtc.signalingserver.domain.conversation.MeshConversation; 9 | import org.nextrtc.signalingserver.domain.conversation.MeshWithMasterConversation; 10 | import org.nextrtc.signalingserver.property.NextRTCProperties; 11 | 12 | import javax.inject.Inject; 13 | 14 | public class ManualConversationFactory extends AbstractConversationFactory { 15 | 16 | private MessageSender sender; 17 | private LeftConversation leftConversation; 18 | private ExchangeSignalsBetweenMembers exchange; 19 | 20 | @Inject 21 | public ManualConversationFactory(LeftConversation leftConversation, 22 | ExchangeSignalsBetweenMembers exchange, 23 | MessageSender sender, 24 | NextRTCProperties properties) { 25 | super(properties); 26 | this.leftConversation = leftConversation; 27 | this.exchange = exchange; 28 | this.sender = sender; 29 | registerConversationType("MESH", this::createMesh); 30 | registerConversationType("BROADCAST", this::createBroadcast); 31 | registerConversationType("MESH_WITH_MASTER", this::createMeshWithMaster); 32 | } 33 | 34 | private Conversation createMesh(String conversationName) { 35 | return new MeshConversation(conversationName, leftConversation, sender, exchange); 36 | } 37 | 38 | private Conversation createMeshWithMaster(String conversationName) { 39 | return new MeshWithMasterConversation(conversationName, leftConversation, sender, exchange); 40 | } 41 | 42 | private Conversation createBroadcast(String conversationName) { 43 | return new BroadcastConversation(conversationName, leftConversation, sender, exchange); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/TestClientActor.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.mockito.Mockito; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static org.mockito.Mockito.*; 11 | 12 | public class TestClientActor { 13 | private Server server; 14 | private String name; 15 | private MockedClient client; 16 | private Connection session; 17 | 18 | public TestClientActor(String name, Server server) { 19 | this.server = server; 20 | this.name = name; 21 | Connection connection = mock(Connection.class); 22 | this.client = new MockedClient(server, connection); 23 | when(connection.getId()).thenReturn(name); 24 | when(connection.isOpen()).thenReturn(true); 25 | doNothing().when(connection).sendObject(Mockito.argThat(client)); 26 | this.session = connection; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return name; 32 | } 33 | 34 | public void openSocket() { 35 | server.register(session); 36 | } 37 | 38 | public void closeSocket() { 39 | server.unregister(session, "Bye"); 40 | } 41 | 42 | public void create(String conversationName, String type) { 43 | Map custom = new HashMap<>(); 44 | custom.put("type", StringUtils.defaultString(type, "MESH")); 45 | server.handle(Message.create() 46 | .signal(Signals.CREATE) 47 | .content(conversationName) 48 | .custom(custom) 49 | .build(), session); 50 | } 51 | 52 | public void join(String conversationName) { 53 | server.handle(Message.create() 54 | .signal(Signals.JOIN) 55 | .content(conversationName) 56 | .build(), session); 57 | } 58 | 59 | public void sendToServer(Message msg) { 60 | server.handle(msg, session); 61 | } 62 | 63 | public Member asMember() { 64 | return new Member(session, null); 65 | } 66 | 67 | public List getMessages() { 68 | return client.getMessages(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/repository/Conversations.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import com.google.common.collect.Maps; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.nextrtc.signalingserver.domain.Conversation; 6 | import org.nextrtc.signalingserver.domain.Member; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.io.IOException; 10 | import java.util.Collection; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | import static org.apache.commons.lang3.StringUtils.isEmpty; 15 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NAME_OCCUPIED; 16 | 17 | @Slf4j 18 | @Repository 19 | public class Conversations implements ConversationRepository { 20 | 21 | private Map conversations = Maps.newConcurrentMap(); 22 | 23 | @Override 24 | public Optional findBy(String id) { 25 | if (isEmpty(id)) { 26 | return Optional.empty(); 27 | } 28 | return Optional.ofNullable(conversations.get(id)); 29 | } 30 | 31 | @Override 32 | public Optional findBy(Member from) { 33 | return conversations.values().stream().filter(conversation -> conversation.has(from)).findAny(); 34 | } 35 | 36 | @Override 37 | public Conversation remove(String id) { 38 | return conversations.remove(id); 39 | } 40 | 41 | @Override 42 | public Conversation save(Conversation conversation) { 43 | if (conversations.containsKey(conversation.getId())) { 44 | throw CONVERSATION_NAME_OCCUPIED.exception(); 45 | } 46 | conversations.put(conversation.getId(), conversation); 47 | return conversation; 48 | } 49 | 50 | @Override 51 | public Collection getAllIds() { 52 | return conversations.keySet(); 53 | } 54 | 55 | @Override 56 | public void close() throws IOException { 57 | for (Conversation conversation : conversations.values()) { 58 | try { 59 | conversation.close(); 60 | } catch (Exception e) { 61 | log.error("Problem during closing conversation " + conversation.getId(), e); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/resolver/AbstractSignalResolver.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.resolver; 2 | 3 | import org.apache.commons.lang3.tuple.ImmutablePair; 4 | import org.apache.commons.lang3.tuple.Pair; 5 | import org.nextrtc.signalingserver.cases.SignalHandler; 6 | import org.nextrtc.signalingserver.domain.Signal; 7 | import org.nextrtc.signalingserver.domain.SignalResolver; 8 | 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.stream.Collectors; 13 | 14 | import static java.util.Optional.ofNullable; 15 | 16 | public abstract class AbstractSignalResolver implements SignalResolver { 17 | 18 | private final Map customHandlers = new ConcurrentHashMap<>(); 19 | 20 | public AbstractSignalResolver(Map handlers) { 21 | customHandlers.put(Signal.EMPTY, (msg) -> { 22 | }); 23 | Map collect = handlers.entrySet().stream().collect(Collectors.toMap(k -> Signal.byHandlerName(k.getKey()), Map.Entry::getValue)); 24 | customHandlers.putAll(collect); 25 | } 26 | 27 | @Override 28 | public Pair resolve(String string) { 29 | Signal signal = Signal.fromString(string); 30 | if (customHandlers.containsKey(signal)) { 31 | return new ImmutablePair<>(signal, customHandlers.get(signal)); 32 | } 33 | return new ImmutablePair<>(Signal.EMPTY, customHandlers.get(Signal.EMPTY)); 34 | } 35 | 36 | @Override 37 | public Optional> addCustomSignal(Signal signal, SignalHandler handler) { 38 | SignalHandler oldValue = customHandlers.put(signal, handler); 39 | if (oldValue == null) { 40 | return Optional.empty(); 41 | } 42 | return Optional.of(new ImmutablePair<>(signal, handler)); 43 | } 44 | 45 | protected void initByDefault() { 46 | for (Signal signal : Signal.values()) { 47 | SignalHandler handler = getHandler(signal); 48 | addCustomSignal(signal, handler); 49 | } 50 | } 51 | 52 | private SignalHandler getHandler(Signal signal) { 53 | return ofNullable(customHandlers.get(signal)).orElse(customHandlers.get(Signal.EMPTY)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/RegisterMember.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 4 | import org.nextrtc.signalingserver.domain.Connection; 5 | import org.nextrtc.signalingserver.domain.Member; 6 | import org.nextrtc.signalingserver.domain.MessageSender; 7 | import org.nextrtc.signalingserver.domain.PingTask; 8 | import org.nextrtc.signalingserver.factory.MemberFactory; 9 | import org.nextrtc.signalingserver.property.NextRTCProperties; 10 | import org.nextrtc.signalingserver.repository.MemberRepository; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.inject.Inject; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.ScheduledFuture; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_OPENED; 19 | 20 | @Component 21 | public class RegisterMember { 22 | 23 | private NextRTCEventBus eventBus; 24 | private NextRTCProperties properties; 25 | private MemberRepository members; 26 | private ScheduledExecutorService scheduler; 27 | private MemberFactory factory; 28 | private MessageSender sender; 29 | 30 | @Inject 31 | public RegisterMember(NextRTCEventBus eventBus, 32 | NextRTCProperties properties, 33 | MemberRepository members, 34 | ScheduledExecutorService scheduler, 35 | MemberFactory factory, 36 | MessageSender sender) { 37 | this.eventBus = eventBus; 38 | this.properties = properties; 39 | this.members = members; 40 | this.scheduler = scheduler; 41 | this.factory = factory; 42 | this.sender = sender; 43 | } 44 | 45 | public void incoming(Connection connection) { 46 | Member newMember = factory.create(connection, ping(connection)); 47 | Member registered = members.register(newMember); 48 | eventBus.post(SESSION_OPENED.occurFor(registered.getConnection())); 49 | } 50 | 51 | private ScheduledFuture ping(Connection connection) { 52 | return scheduler.scheduleAtFixedRate(new PingTask(connection, sender), 1, 53 | properties.getPingPeriod(), TimeUnit.SECONDS); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/JoinConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.nextrtc.signalingserver.domain.Conversation; 4 | import org.nextrtc.signalingserver.domain.InternalMessage; 5 | import org.nextrtc.signalingserver.domain.Signals; 6 | import org.nextrtc.signalingserver.exception.SignalingException; 7 | import org.nextrtc.signalingserver.property.NextRTCProperties; 8 | import org.nextrtc.signalingserver.repository.ConversationRepository; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | import java.util.Optional; 13 | 14 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NOT_FOUND; 15 | import static org.nextrtc.signalingserver.exception.Exceptions.MEMBER_IN_OTHER_CONVERSATION; 16 | 17 | @Component(Signals.JOIN_HANDLER) 18 | public class JoinConversation implements SignalHandler { 19 | 20 | private ConversationRepository conversations; 21 | private CreateConversation createConversation; 22 | private NextRTCProperties properties; 23 | 24 | @Inject 25 | public JoinConversation(ConversationRepository conversations, 26 | CreateConversation createConversation, 27 | NextRTCProperties properties) { 28 | this.conversations = conversations; 29 | this.createConversation = createConversation; 30 | this.properties = properties; 31 | } 32 | 33 | public synchronized void execute(InternalMessage context) { 34 | conversations.findBy(context.getFrom()) 35 | .map(Conversation::getId) 36 | .map(MEMBER_IN_OTHER_CONVERSATION::exception) 37 | .ifPresent(SignalingException::throwException); 38 | 39 | Optional conversation = findConversationToJoin(context); 40 | 41 | if (conversation.isPresent()) { 42 | conversation.get().join(context.getFrom()); 43 | } else { 44 | if (properties.isJoinOnlyToExisting()) { 45 | throw CONVERSATION_NOT_FOUND.exception(); 46 | } else { 47 | createConversation.execute(context); 48 | } 49 | } 50 | } 51 | 52 | private Optional findConversationToJoin(InternalMessage message) { 53 | return conversations.findBy(message.getContent()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/RTCConnections.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import com.google.common.collect.HashBasedTable; 4 | import com.google.common.collect.Table; 5 | import org.nextrtc.signalingserver.cases.connection.ConnectionContext; 6 | import org.nextrtc.signalingserver.property.NextRTCProperties; 7 | import org.springframework.context.annotation.Scope; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.inject.Inject; 12 | import java.io.Closeable; 13 | import java.io.IOException; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.stream.Collectors; 19 | 20 | @Component 21 | @Scope("singleton") 22 | public class RTCConnections implements Closeable{ 23 | private static Table connections = HashBasedTable.create(); 24 | 25 | private ScheduledExecutorService scheduler; 26 | private NextRTCProperties properties; 27 | 28 | @Inject 29 | public RTCConnections(ScheduledExecutorService scheduler, NextRTCProperties properties) { 30 | this.scheduler = scheduler; 31 | this.properties = properties; 32 | } 33 | 34 | @PostConstruct 35 | void cleanOldConnections() { 36 | scheduler.scheduleWithFixedDelay(this::removeOldConnections, 37 | properties.getMaxConnectionSetupTime(), 38 | properties.getMaxConnectionSetupTime(), 39 | TimeUnit.SECONDS); 40 | } 41 | 42 | void removeOldConnections() { 43 | List oldConnections = connections.values().stream() 44 | .filter(context -> !context.isCurrent()) 45 | .collect(Collectors.toList()); 46 | oldConnections.forEach(c -> connections.remove(c.getMaster(), c.getSlave())); 47 | } 48 | 49 | public void put(Member from, Member to, ConnectionContext ctx) { 50 | connections.put(from, to, ctx); 51 | connections.put(to, from, ctx); 52 | } 53 | 54 | public Optional get(Member from, Member to) { 55 | return Optional.ofNullable(connections.get(from, to)); 56 | } 57 | 58 | 59 | @Override 60 | public void close() throws IOException { 61 | connections.clear(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/modules/NextRTCBeans.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.modules; 2 | 3 | import dagger.Binds; 4 | import dagger.Module; 5 | import dagger.Provides; 6 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 7 | import org.nextrtc.signalingserver.cases.SignalHandler; 8 | import org.nextrtc.signalingserver.domain.DefaultMessageSender; 9 | import org.nextrtc.signalingserver.domain.MessageSender; 10 | import org.nextrtc.signalingserver.domain.SignalResolver; 11 | import org.nextrtc.signalingserver.domain.resolver.ManualSignalResolver; 12 | import org.nextrtc.signalingserver.eventbus.ManualEventDispatcher; 13 | import org.nextrtc.signalingserver.property.ManualNextRTCProperties; 14 | import org.nextrtc.signalingserver.property.NextRTCProperties; 15 | 16 | import javax.inject.Singleton; 17 | import java.util.Map; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.ScheduledExecutorService; 20 | 21 | @Module 22 | public abstract class NextRTCBeans { 23 | 24 | @Provides 25 | @Singleton 26 | static NextRTCEventBus NextRTCEventBus() { 27 | return new NextRTCEventBus(); 28 | } 29 | 30 | @Provides 31 | @Singleton 32 | static ScheduledExecutorService ScheduledExecutorService(NextRTCProperties properties) { 33 | return Executors.newScheduledThreadPool(properties.getMaxConnectionSetupTime()); 34 | } 35 | 36 | @Provides 37 | @Singleton 38 | static ManualNextRTCProperties ManualNextRTCProperties() { 39 | return new ManualNextRTCProperties(); 40 | } 41 | 42 | @Binds 43 | abstract NextRTCProperties NextRTCProperties(ManualNextRTCProperties properties); 44 | 45 | @Provides 46 | @Singleton 47 | static ManualSignalResolver ManualSignalResolver(Map signals) { 48 | return new ManualSignalResolver(signals); 49 | } 50 | 51 | @Binds 52 | abstract SignalResolver signalResolver(ManualSignalResolver manualSignalResolver); 53 | 54 | @Binds 55 | abstract MessageSender messageSender(DefaultMessageSender messageSender); 56 | 57 | 58 | @Provides 59 | @Singleton 60 | static ManualEventDispatcher ManualEventDispatcher(NextRTCEventBus eventBus) { 61 | ManualEventDispatcher manualEventDispatcher = new ManualEventDispatcher(eventBus); 62 | eventBus.register(manualEventDispatcher); 63 | return manualEventDispatcher; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/MessageMatcher.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.mockito.ArgumentMatcher; 5 | import org.nextrtc.signalingserver.domain.Message; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.function.Predicate; 12 | 13 | @Slf4j 14 | public class MessageMatcher extends ArgumentMatcher { 15 | 16 | private final List filter; 17 | private List messages = Collections.synchronizedList(new ArrayList<>()); 18 | 19 | public MessageMatcher() { 20 | this.filter = Arrays.asList("ping"); 21 | } 22 | 23 | public MessageMatcher(String... filter) { 24 | this.filter = Arrays.asList(filter); 25 | } 26 | 27 | @Override 28 | public boolean matches(Object argument) { 29 | if (argument instanceof Message) { 30 | Message msg = (Message) argument; 31 | if (!filter.contains(msg.getSignal())) { 32 | messages.add(msg); 33 | } 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | public Message getMessage() { 40 | return messages.get(0); 41 | } 42 | 43 | public Message getMessage(int number) { 44 | if (messages.size() <= number) { 45 | return null; 46 | } 47 | return messages.get(number); 48 | } 49 | 50 | public void reset() { 51 | messages.clear(); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | StringBuilder sb = new StringBuilder(); 57 | for (Message msg : messages) { 58 | sb.append(msg); 59 | sb.append(", "); 60 | } 61 | return sb.toString(); 62 | } 63 | 64 | public List getMessages() { 65 | return this.messages; 66 | } 67 | 68 | public CheckMessage has(Predicate condition) { 69 | return new CheckMessage().has(condition); 70 | } 71 | 72 | public class CheckMessage { 73 | private List> predicates = new ArrayList<>(); 74 | 75 | public CheckMessage has(Predicate condition) { 76 | predicates.add(condition); 77 | return this; 78 | } 79 | 80 | public boolean check() { 81 | return messages.stream() 82 | .filter(this::matchAllPredicates) 83 | .count() > 0; 84 | } 85 | 86 | private boolean matchAllPredicates(final Message m) { 87 | return predicates.stream().filter(p -> p.test(m)).count() == predicates.size(); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/LeftConversationTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.domain.Member; 8 | import org.nextrtc.signalingserver.domain.Signal; 9 | import org.nextrtc.signalingserver.repository.Conversations; 10 | import org.nextrtc.signalingserver.repository.Members; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import static org.junit.Assert.assertFalse; 14 | import static org.nextrtc.signalingserver.domain.InternalMessage.create; 15 | 16 | public class LeftConversationTest extends BaseTest { 17 | 18 | @Rule 19 | public ExpectedException exception = ExpectedException.none(); 20 | 21 | @Autowired 22 | private Members members; 23 | 24 | @Autowired 25 | private Conversations conversations; 26 | 27 | @Autowired 28 | private LeftConversation leftConversation; 29 | 30 | @Test 31 | public void shouldThrowAnExceptionWhenConversationDoesntExists() throws Exception { 32 | // given 33 | Member john = mockMember("Jan"); 34 | members.register(john); 35 | 36 | // then 37 | exception.expectMessage("CONVERSATION_NOT_FOUND"); 38 | 39 | // when 40 | leftConversation.execute(create() 41 | .signal(Signal.LEFT) 42 | .from(john) 43 | .build()); 44 | } 45 | 46 | @Test 47 | public void shouldLeaveConversation() throws Exception { 48 | // given 49 | Member john = mockMember("Jan"); 50 | members.register(john); 51 | createConversation("conversationId", john); 52 | 53 | // when 54 | leftConversation.execute(create() 55 | .from(john) 56 | .build()); 57 | 58 | // then 59 | assertFalse(john.getConversation().isPresent()); 60 | } 61 | 62 | @Test 63 | public void shouldRemoveConversationIfLastMemberLeft() throws Exception { 64 | // given 65 | Member john = mockMember("Jan"); 66 | Member stan = mockMember("Stan"); 67 | members.register(john); 68 | createConversation("conversationId", john); 69 | joinConversation("conversationId", stan); 70 | 71 | // when 72 | leftConversation.execute(create() 73 | .from(john) 74 | .build()); 75 | leftConversation.execute(create() 76 | .from(stan) 77 | .build()); 78 | 79 | // then 80 | assertFalse(john.getConversation().isPresent()); 81 | assertFalse(stan.getConversation().isPresent()); 82 | assertFalse(conversations.findBy("conversationId").isPresent()); 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/DefaultMessageSender.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nextrtc.signalingserver.repository.MemberRepository; 5 | import org.springframework.context.annotation.Scope; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.inject.Inject; 9 | 10 | @Component 11 | @Scope("singleton") 12 | @Slf4j 13 | public class DefaultMessageSender implements MessageSender { 14 | 15 | private MemberRepository members; 16 | 17 | @Inject 18 | public DefaultMessageSender(MemberRepository members) { 19 | this.members = members; 20 | } 21 | 22 | @Override 23 | public void send(InternalMessage message) { 24 | send(message, 3); 25 | } 26 | 27 | private void send(InternalMessage message, int retry) { 28 | if (message.getSignal() != Signal.PING) { 29 | log.debug("Outgoing: " + message.transformToExternalMessage()); 30 | } 31 | if (message.getSignal() == Signal.ERROR) { 32 | tryToSendErrorMessage(message); 33 | return; 34 | } 35 | Member destination = message.getTo(); 36 | if (destination == null || !destination.getConnection().isOpen()) { 37 | log.warn("Destination member is not set or session is closed! Message will not be send: " + message.transformToExternalMessage()); 38 | return; 39 | } 40 | members.findBy(destination.getId()).ifPresent(member -> 41 | lockAndRun(message, member, retry) 42 | ); 43 | } 44 | 45 | private void tryToSendErrorMessage(InternalMessage message) { 46 | try { 47 | Connection connection = message.getTo().getConnection(); 48 | synchronized (connection) { 49 | connection.sendObject(message.transformToExternalMessage()); 50 | } 51 | } catch (Exception e) { 52 | throw new RuntimeException("Unable to send message: " + message.transformToExternalMessage(), e); 53 | } 54 | } 55 | 56 | private void lockAndRun(InternalMessage message, Member destination, int retry) { 57 | try { 58 | Connection connection = destination.getConnection(); 59 | synchronized (destination) { 60 | connection.sendObject(message.transformToExternalMessage()); 61 | } 62 | } catch (Exception e) { 63 | if (retry >= 0) { 64 | log.warn("Retrying... " + message.transformToExternalMessage()); 65 | send(message, --retry); 66 | } 67 | log.error("Unable to send message: " + message.transformToExternalMessage() + " error during sending!"); 68 | throw new RuntimeException("Unable to send message: " + message.transformToExternalMessage(), e); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/factory/ConversationFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.factory; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.domain.Conversation; 8 | import org.nextrtc.signalingserver.domain.conversation.BroadcastConversation; 9 | import org.nextrtc.signalingserver.domain.conversation.MeshConversation; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | import static org.hamcrest.Matchers.*; 13 | import static org.junit.Assert.*; 14 | 15 | public class ConversationFactoryTest extends BaseTest { 16 | 17 | @Autowired 18 | private ConversationFactory conversationFactory; 19 | 20 | @Rule 21 | public ExpectedException expect = ExpectedException.none(); 22 | 23 | @Test 24 | public void shouldCreateConversation() throws Exception { 25 | // given 26 | 27 | // when 28 | Conversation createdConversation = conversationFactory.create(null, null); 29 | 30 | // then 31 | assertNotNull(createdConversation); 32 | assertNotNull(createdConversation.getId()); 33 | } 34 | 35 | @Test 36 | public void shouldCreateConversationWithRandomNameOnEmptyConversationName() throws Exception { 37 | // given 38 | 39 | // then 40 | final Conversation conversation = conversationFactory.create("", null); 41 | 42 | // when 43 | assertNotNull(conversation); 44 | assertThat(conversation.getId(), not(nullValue())); 45 | 46 | } 47 | 48 | @Test 49 | public void shouldCreateBroadcastConversationWhenInCustomPayloadTypeIsBroadcast() throws Exception { 50 | // given 51 | 52 | // when 53 | Conversation conversation = conversationFactory.create("new conversation", "BROADCAST"); 54 | 55 | // then 56 | assertThat(conversation.getId(), is("new conversation")); 57 | assertTrue(conversation instanceof BroadcastConversation); 58 | } 59 | 60 | @Test 61 | public void shouldCreateMeshConversationWhenInCustomPayloadTypeIsMesh() throws Exception { 62 | // given 63 | 64 | // when 65 | Conversation conversation = conversationFactory.create("new conversation", "MESH"); 66 | 67 | // then 68 | assertThat(conversation.getId(), is("new conversation")); 69 | assertTrue(conversation instanceof MeshConversation); 70 | } 71 | 72 | @Test 73 | public void shouldCreateMeshConversationByDefault() throws Exception { 74 | // given 75 | 76 | // when 77 | Conversation conversation = conversationFactory.create("new conversation", null); 78 | 79 | // then 80 | assertNotNull(conversation.getId()); 81 | assertTrue(conversation instanceof MeshConversation); 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Member.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import lombok.Getter; 4 | import org.apache.commons.lang3.builder.EqualsBuilder; 5 | import org.apache.commons.lang3.builder.HashCodeBuilder; 6 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 7 | import org.nextrtc.signalingserver.api.dto.NextRTCMember; 8 | import org.springframework.context.annotation.Scope; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | import java.util.Optional; 13 | import java.util.concurrent.ScheduledFuture; 14 | 15 | import static org.nextrtc.signalingserver.api.NextRTCEvents.MEMBER_JOINED; 16 | import static org.nextrtc.signalingserver.api.NextRTCEvents.MEMBER_LEFT; 17 | import static org.nextrtc.signalingserver.domain.EventContext.builder; 18 | 19 | @Getter 20 | @Component 21 | @Scope("prototype") 22 | public class Member implements NextRTCMember { 23 | 24 | private String id; 25 | private Connection connection; 26 | private Conversation conversation; 27 | 28 | private NextRTCEventBus eventBus; 29 | 30 | private ScheduledFuture ping; 31 | 32 | public Member(Connection connection, ScheduledFuture ping) { 33 | this.id = connection.getId(); 34 | this.connection = connection; 35 | this.ping = ping; 36 | } 37 | 38 | public Optional getConversation() { 39 | return Optional.ofNullable(conversation); 40 | } 41 | 42 | public void markLeft() { 43 | ping.cancel(true); 44 | } 45 | 46 | void assign(Conversation conversation) { 47 | this.conversation = conversation; 48 | eventBus.post(MEMBER_JOINED.basedOn( 49 | builder() 50 | .conversation(conversation) 51 | .from(this))); 52 | } 53 | 54 | public void unassignConversation(Conversation conversation) { 55 | eventBus.post(MEMBER_LEFT.basedOn( 56 | builder() 57 | .conversation(conversation) 58 | .from(this))); 59 | this.conversation = null; 60 | } 61 | 62 | public boolean hasSameConversation(Member to) { 63 | return to != null && conversation.equals(to.conversation); 64 | } 65 | 66 | @Inject 67 | public void setEventBus(NextRTCEventBus eventBus) { 68 | this.eventBus = eventBus; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return String.format("%s", id); 74 | } 75 | 76 | public synchronized Connection getConnection() { 77 | return connection; 78 | } 79 | 80 | @Override 81 | public boolean equals(Object o) { 82 | if (!(o instanceof Member)) { 83 | return false; 84 | } 85 | Member m = (Member) o; 86 | return new EqualsBuilder()// 87 | .append(m.id, id)// 88 | .isEquals(); 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return new HashCodeBuilder()// 94 | .append(id)// 95 | .build(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/modules/NextRTCSignals.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.modules; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import dagger.multibindings.IntoMap; 6 | import dagger.multibindings.StringKey; 7 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 8 | import org.nextrtc.signalingserver.cases.*; 9 | import org.nextrtc.signalingserver.domain.MessageSender; 10 | import org.nextrtc.signalingserver.domain.Signals; 11 | import org.nextrtc.signalingserver.factory.ConversationFactory; 12 | import org.nextrtc.signalingserver.property.NextRTCProperties; 13 | import org.nextrtc.signalingserver.repository.ConversationRepository; 14 | 15 | import javax.inject.Singleton; 16 | 17 | @Module 18 | public abstract class NextRTCSignals { 19 | 20 | 21 | @Provides 22 | @Singleton 23 | @IntoMap 24 | @StringKey(Signals.ANSWER_RESPONSE_HANDLER) 25 | static SignalHandler AnswerResponseHandler() { 26 | return new AnswerResponseHandler(); 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | @IntoMap 32 | @StringKey(Signals.CANDIDATE_HANDLER) 33 | static SignalHandler CandidateHandler() { 34 | return new CandidateHandler(); 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | @IntoMap 40 | @StringKey(Signals.CREATE_HANDLER) 41 | static SignalHandler CreateConversationEntry(CreateConversation conversation) { 42 | return conversation; 43 | } 44 | 45 | @Provides 46 | @Singleton 47 | static CreateConversation CreateConversation(NextRTCEventBus eventBus, 48 | ConversationRepository conversations, 49 | ConversationFactory factory) { 50 | return new CreateConversation(eventBus, conversations, factory); 51 | } 52 | 53 | @Provides 54 | @Singleton 55 | @IntoMap 56 | @StringKey(Signals.JOIN_HANDLER) 57 | static SignalHandler JoinConversation(ConversationRepository conversations, 58 | CreateConversation create, 59 | NextRTCProperties properties) { 60 | return new JoinConversation(conversations, create, properties); 61 | } 62 | 63 | @Provides 64 | @Singleton 65 | @IntoMap 66 | @StringKey(Signals.LEFT_HANDLER) 67 | static SignalHandler LeftConversation(NextRTCEventBus eventBus, 68 | ConversationRepository conversations) { 69 | return new LeftConversation(eventBus, conversations); 70 | } 71 | 72 | @Provides 73 | @Singleton 74 | @IntoMap 75 | @StringKey(Signals.OFFER_RESPONSE_HANDLER) 76 | static SignalHandler OfferResponseHandler() { 77 | return new OfferResponseHandler(); 78 | } 79 | 80 | @Provides 81 | @Singleton 82 | @IntoMap 83 | @StringKey(Signals.TEXT_HANDLER) 84 | static SignalHandler TextMessage(NextRTCEventBus eventBus, 85 | MessageSender sender) { 86 | return new TextMessage(eventBus, sender); 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/api/NextRTCEvents.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 4 | import org.nextrtc.signalingserver.api.dto.NextRTCMember; 5 | import org.nextrtc.signalingserver.domain.Connection; 6 | import org.nextrtc.signalingserver.domain.Conversation; 7 | import org.nextrtc.signalingserver.domain.EventContext; 8 | import org.nextrtc.signalingserver.domain.InternalMessage; 9 | import org.nextrtc.signalingserver.exception.Exceptions; 10 | 11 | public enum NextRTCEvents { 12 | SESSION_OPENED, 13 | SESSION_CLOSED, 14 | CONVERSATION_CREATED, 15 | CONVERSATION_DESTROYED, 16 | UNEXPECTED_SITUATION, 17 | MEMBER_JOINED, 18 | MEMBER_LEFT, 19 | MEDIA_LOCAL_STREAM_REQUESTED, 20 | MEDIA_LOCAL_STREAM_CREATED, 21 | MEDIA_STREAMING, 22 | TEXT, 23 | MESSAGE; 24 | 25 | public NextRTCEvent basedOn(InternalMessage message, Conversation conversation) { 26 | return EventContext.builder() 27 | .from(message.getFrom()) 28 | .to(message.getTo()) 29 | .custom(message.getCustom()) 30 | .conversation(conversation) 31 | .type(this) 32 | .build(); 33 | } 34 | 35 | public NextRTCEvent basedOn(EventContext.EventContextBuilder builder) { 36 | return builder 37 | .type(this) 38 | .build(); 39 | } 40 | 41 | public NextRTCEvent occurFor(Connection connection, String reason) { 42 | return EventContext.builder() 43 | .from(new InternalMember(connection)) 44 | .type(this) 45 | .exception(Exceptions.UNKNOWN_ERROR.exception()) 46 | .content(reason) 47 | .build(); 48 | } 49 | 50 | public NextRTCEvent occurFor(Connection connection) { 51 | return EventContext.builder() 52 | .type(this) 53 | .from(new InternalMember(connection)) 54 | .exception(Exceptions.UNKNOWN_ERROR.exception()) 55 | .build(); 56 | } 57 | 58 | public NextRTCEvent basedOn(InternalMessage message) { 59 | return EventContext.builder() 60 | .from(message.getFrom()) 61 | .to(message.getTo()) 62 | .custom(message.getCustom()) 63 | .content(message.getContent()) 64 | .conversation(message.getFrom().getConversation().orElse(null)) 65 | .type(this) 66 | .build(); 67 | } 68 | 69 | private static class InternalMember implements NextRTCMember { 70 | 71 | private final Connection connection; 72 | 73 | InternalMember(Connection connection) { 74 | this.connection = connection; 75 | } 76 | 77 | @Override 78 | public Connection getConnection() { 79 | return connection; 80 | } 81 | 82 | @Override 83 | public String getId() { 84 | if (connection == null) { 85 | return null; 86 | } 87 | return connection.getId(); 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return getId(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Conversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import lombok.Getter; 4 | import org.nextrtc.signalingserver.api.dto.NextRTCConversation; 5 | import org.nextrtc.signalingserver.cases.LeftConversation; 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.inject.Inject; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | 13 | @Getter 14 | @Component 15 | @Scope("prototype") 16 | public abstract class Conversation implements NextRTCConversation { 17 | protected static final ExecutorService parallel = Executors.newCachedThreadPool(); 18 | protected final String id; 19 | 20 | private LeftConversation leftConversation; 21 | protected MessageSender messageSender; 22 | 23 | public Conversation(String id) { 24 | this.id = id; 25 | } 26 | 27 | public Conversation(String id, 28 | LeftConversation leftConversation, 29 | MessageSender messageSender) { 30 | this.id = id; 31 | this.leftConversation = leftConversation; 32 | this.messageSender = messageSender; 33 | } 34 | 35 | public abstract void join(Member sender); 36 | 37 | public synchronized void left(Member sender) { 38 | if (remove(sender) && isWithoutMember()) { 39 | leftConversation.destroy(this, sender); 40 | } 41 | } 42 | 43 | protected abstract boolean remove(Member leaving); 44 | 45 | protected void assignSenderToConversation(Member sender) { 46 | sender.assign(this); 47 | } 48 | 49 | public abstract boolean isWithoutMember(); 50 | 51 | public abstract boolean has(Member from); 52 | 53 | public abstract void exchangeSignals(InternalMessage message); 54 | 55 | protected void sendJoinedToConversation(Member sender, String id) { 56 | messageSender.send(InternalMessage.create()// 57 | .to(sender)// 58 | .content(id)// 59 | .signal(Signal.JOINED)// 60 | .build()); 61 | } 62 | 63 | protected void sendJoinedFrom(Member sender, Member member) { 64 | messageSender.send(InternalMessage.create()// 65 | .from(sender)// 66 | .to(member)// 67 | .signal(Signal.NEW_JOINED)// 68 | .content(sender.getId()) 69 | .build()); 70 | } 71 | 72 | protected void sendLeftMessage(Member leaving, Member recipient) { 73 | messageSender.send(InternalMessage.create()// 74 | .from(leaving)// 75 | .to(recipient)// 76 | .signal(Signal.LEFT)// 77 | .build()); 78 | } 79 | 80 | public abstract void broadcast(Member from, InternalMessage message); 81 | 82 | @Inject 83 | public void setLeftConversation(LeftConversation leftConversation) { 84 | this.leftConversation = leftConversation; 85 | } 86 | 87 | @Inject 88 | public void setMessageSender(MessageSender messageSender) { 89 | this.messageSender = messageSender; 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | return this.getClass().getSimpleName() + ": " + id; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/repository/ConversationsTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.repository; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.domain.Conversation; 8 | import org.nextrtc.signalingserver.factory.ConversationFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.hamcrest.Matchers.containsString; 14 | import static org.hamcrest.Matchers.is; 15 | import static org.junit.Assert.*; 16 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NAME_OCCUPIED; 17 | 18 | public class ConversationsTest extends BaseTest { 19 | 20 | @Autowired 21 | private Conversations conversations; 22 | 23 | @Autowired 24 | private ConversationFactory conversationFactory; 25 | 26 | @Rule 27 | public ExpectedException expect = ExpectedException.none(); 28 | 29 | @Test 30 | public void shouldFindExistingConversation() throws Exception { 31 | // given 32 | Conversation conversation = conversations.save(conversationFactory.create("new", null)); 33 | 34 | // when 35 | Optional found = conversations.findBy("new"); 36 | 37 | // then 38 | assertTrue(found.isPresent()); 39 | Conversation actual = found.get(); 40 | assertEquals(conversation, actual); 41 | assertEquals(conversation.getId(), actual.getId()); 42 | } 43 | 44 | @Test 45 | public void shouldThrowExceptionWhenConversationNameIsOccupied() throws Exception { 46 | // given 47 | Conversation conversation = conversationFactory.create("aaaa", null); 48 | conversations.save(conversation); 49 | 50 | // then 51 | expect.expectMessage(containsString(CONVERSATION_NAME_OCCUPIED.name())); 52 | 53 | // when 54 | conversations.save(conversation); 55 | } 56 | 57 | @Test 58 | public void shouldRemoveConversation() { 59 | // given 60 | Conversation saved = conversations.save(conversationFactory.create("new", null)); 61 | 62 | // when 63 | Conversation removed = conversations.remove("new"); 64 | 65 | // then 66 | assertThat(removed, is(saved)); 67 | assertFalse(conversations.findBy("new").isPresent()); 68 | 69 | } 70 | 71 | @Test 72 | public void shouldNotFindConversationWhenMemberIsDifferent() { 73 | // given 74 | Conversation saved = conversations.save(conversationFactory.create("new", null)); 75 | saved.join(mockMember("BBBB")); 76 | // when 77 | Optional member = conversations.findBy(mockMember("AAAA")); 78 | 79 | // then 80 | assertFalse(member.isPresent()); 81 | } 82 | 83 | @Test 84 | public void shouldFindConversationWhenMemberIsInConversation() { 85 | // given 86 | Conversation saved = conversations.save(conversationFactory.create("new", null)); 87 | saved.join(mockMember("BBBB")); 88 | saved.join(mockMember("AAAA")); 89 | // when 90 | Optional member = conversations.findBy(mockMember("AAAA")); 91 | 92 | // then 93 | assertTrue(member.isPresent()); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/api/DecoderTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.api; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.domain.Message; 5 | 6 | import static org.apache.commons.lang3.StringUtils.EMPTY; 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.hamcrest.Matchers.is; 9 | import static org.hamcrest.Matchers.isEmptyOrNullString; 10 | import static org.junit.Assert.assertNotNull; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class DecoderTest { 14 | 15 | private NextRTCServer.MessageDecoder decoder = new NextRTCServer.MessageDecoder(); 16 | 17 | @Test 18 | public void shouldParseBasicObject() { 19 | // given 20 | String validJson = "{'from' : 'Alice',"// 21 | + "'to' : 'Bob',"// 22 | + "'signal' : 'join',"// 23 | + "'content' : 'something'}"; 24 | 25 | // when 26 | Message result = decoder.decode(validJson); 27 | 28 | // then 29 | assertNotNull(result); 30 | assertThat(result.getFrom(), is("Alice")); 31 | assertThat(result.getTo(), is("Bob")); 32 | assertThat(result.getSignal(), is("join")); 33 | assertThat(result.getContent(), is("something")); 34 | } 35 | 36 | @Test 37 | public void shouldParseAlmostEmptyObject() { 38 | // given 39 | String validJson = "{'signal' : 'join',"// 40 | + "'content' : 'something'}"; 41 | 42 | // when 43 | Message result = decoder.decode(validJson); 44 | 45 | // then 46 | assertNotNull(result); 47 | assertThat(result.getFrom(), is(EMPTY)); 48 | assertThat(result.getTo(), is(EMPTY)); 49 | assertThat(result.getSignal(), is("join")); 50 | assertThat(result.getContent(), is("something")); 51 | } 52 | 53 | @Test 54 | public void shouldRecognizeAndDisposeXSSAttack() { 55 | // given 56 | String validJson = "{'signal' : 'join',"// 57 | + "'content':''}"; 58 | 59 | // when 60 | Message result = decoder.decode(validJson); 61 | 62 | // then 63 | assertNotNull(result); 64 | assertThat(result.getContent(), containsString("<script>alert")); 65 | } 66 | 67 | @Test 68 | public void shouldParseRequestWithDoubleQuotes() { 69 | // given 70 | String validJson = "{'from' : 'Alice',"// 71 | + "'to' : 'Bob',"// 72 | + "'signal' : 'join',"// 73 | + "'content' : 'something',"// 74 | + "'parameters' : {'param1' : 'value1'}}".replace("'", "\""); 75 | 76 | // when 77 | Message result = decoder.decode(validJson); 78 | 79 | // then 80 | assertNotNull(result); 81 | assertThat(result.getFrom(), is("Alice")); 82 | assertThat(result.getTo(), is("Bob")); 83 | assertThat(result.getSignal(), is("join")); 84 | assertThat(result.getContent(), is("something")); 85 | } 86 | 87 | @Test 88 | public void shouldNormalRequest() { 89 | // given 90 | String validJson = "{'from' : 'Alice',"// 91 | + "'to' : null,"// 92 | + "'signal' : 'join',"// 93 | + "'content' : 'aaa'}".replace("'", "\""); 94 | 95 | // when 96 | Message result = decoder.decode(validJson); 97 | 98 | // then 99 | assertNotNull(result); 100 | assertThat(result.getFrom(), is("Alice")); 101 | assertThat(result.getTo(), isEmptyOrNullString()); 102 | assertThat(result.getSignal(), is("join")); 103 | assertThat(result.getContent(), is("aaa")); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/InternalMessage.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import com.google.common.collect.Maps; 4 | import lombok.Getter; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.Map; 8 | 9 | import static org.apache.commons.lang3.StringUtils.EMPTY; 10 | import static org.apache.commons.lang3.StringUtils.defaultString; 11 | 12 | @Slf4j 13 | @Getter 14 | public class InternalMessage { 15 | 16 | private Member from; 17 | private Member to; 18 | private Signal signal; 19 | private String content; 20 | private Map custom = Maps.newHashMap(); 21 | 22 | private InternalMessage(Member from, Member to, Signal signal, String content, Map custom) { 23 | this.from = from; 24 | this.to = to; 25 | this.signal = signal; 26 | this.content = content; 27 | if (custom != null) { 28 | this.custom.putAll(custom); 29 | } 30 | } 31 | 32 | public static InternalMessageBuilder create() { 33 | return new InternalMessageBuilder(); 34 | } 35 | 36 | public InternalMessageBuilder copy() { 37 | return new InternalMessageBuilder().from(from).to(to).content(content).custom(custom).signal(signal); 38 | } 39 | 40 | public Message transformToExternalMessage() { 41 | return Message.create()// 42 | .from(fromNullable(from))// 43 | .to(fromNullable(to))// 44 | .signal(signal.ordinaryName())// 45 | .content(defaultString(content))// 46 | .custom(custom)// 47 | .build(); 48 | } 49 | 50 | private String fromNullable(Member member) { 51 | return member == null ? EMPTY : member.getId(); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return String.format("(%s -> %s)[%s]: %s |%s", from, to, signal != null ? signal.ordinaryName() : null, content, custom); 57 | } 58 | 59 | public static class InternalMessageBuilder { 60 | private Member from; 61 | private Member to; 62 | private Signal signal; 63 | private String content; 64 | private Map custom = Maps.newHashMap(); 65 | 66 | InternalMessageBuilder() { 67 | } 68 | 69 | public InternalMessage.InternalMessageBuilder from(Member from) { 70 | this.from = from; 71 | return this; 72 | } 73 | 74 | public InternalMessage.InternalMessageBuilder to(Member to) { 75 | this.to = to; 76 | return this; 77 | } 78 | 79 | public InternalMessage.InternalMessageBuilder signal(Signal signal) { 80 | this.signal = signal; 81 | return this; 82 | } 83 | 84 | public InternalMessage.InternalMessageBuilder content(String content) { 85 | this.content = content; 86 | return this; 87 | } 88 | 89 | public InternalMessage.InternalMessageBuilder custom(Map custom) { 90 | if (custom != null) { 91 | this.custom.putAll(custom); 92 | } 93 | return this; 94 | } 95 | 96 | public InternalMessage.InternalMessageBuilder addCustom(String key, String value) { 97 | this.custom.put(key, value); 98 | return this; 99 | } 100 | 101 | public InternalMessage build() { 102 | return new InternalMessage(from, to, signal, content, custom); 103 | } 104 | 105 | public String toString() { 106 | return "InternalMessageBuilder(from=" + this.from + ", to=" + this.to + ", signal=" + this.signal + ", content=" + this.content + ", custom=" + this.custom + ")"; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/eventbus/EventBusTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.eventbus; 2 | 3 | import com.google.common.eventbus.Subscribe; 4 | import org.junit.After; 5 | import org.junit.Test; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.Names; 8 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 9 | import org.nextrtc.signalingserver.api.NextRTCEvents; 10 | import org.nextrtc.signalingserver.api.NextRTCHandler; 11 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 12 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 13 | import org.nextrtc.signalingserver.domain.EventContext; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.test.context.ContextConfiguration; 18 | 19 | import static org.hamcrest.Matchers.is; 20 | import static org.hamcrest.Matchers.nullValue; 21 | import static org.junit.Assert.assertThat; 22 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_CLOSED; 23 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_OPENED; 24 | 25 | @ContextConfiguration(classes = {T1.class, T2.class, T3.class}) 26 | public class EventBusTest extends BaseTest { 27 | 28 | @Autowired 29 | @Qualifier(Names.EVENT_BUS) 30 | private NextRTCEventBus eventBus; 31 | 32 | @Autowired 33 | @Qualifier("t1") 34 | private T1 t1; 35 | 36 | @Autowired 37 | @Qualifier("t2") 38 | private T2 t2; 39 | 40 | @Autowired 41 | @Qualifier("t3") 42 | private T3 t3; 43 | 44 | @Test 45 | public void shouldRegisterListenerWithNextRTCEventListenerAnnotation() throws InterruptedException { 46 | // given 47 | Object object = new Object(); 48 | 49 | // when 50 | eventBus.post(object); 51 | 52 | // then 53 | assertThat(t1.getO(), is(object)); 54 | } 55 | 56 | @Test 57 | public void shouldCallHandleEventMethod() throws Exception { 58 | // given 59 | NextRTCEvent event = event(SESSION_OPENED); 60 | NextRTCEvent notValidEvent = event(SESSION_CLOSED); 61 | 62 | // when 63 | eventBus.post(event); 64 | eventBus.post(notValidEvent); 65 | 66 | // then 67 | assertThat(t2.getEvent(), is(event)); 68 | assertThat(t3.getEvent(), nullValue()); 69 | } 70 | 71 | private NextRTCEvent event(final NextRTCEvents event) { 72 | return EventContext.builder().type(event).build(); 73 | } 74 | 75 | @After 76 | public void resetClass() { 77 | t1.setO(null); 78 | t2.setEvent(null); 79 | t3.setEvent(null); 80 | } 81 | } 82 | 83 | @Component("t1") 84 | @NextRTCEventListener 85 | class T1 { 86 | 87 | private Object o; 88 | 89 | @Subscribe 90 | public void callMe(Object o) { 91 | this.o = o; 92 | } 93 | 94 | public Object getO() { 95 | return this.o; 96 | } 97 | 98 | public void setO(Object o) { 99 | this.o = o; 100 | } 101 | } 102 | 103 | @Component("t2") 104 | @NextRTCEventListener(SESSION_OPENED) 105 | class T2 implements NextRTCHandler { 106 | 107 | private NextRTCEvent event; 108 | 109 | @Override 110 | public void handleEvent(NextRTCEvent event) { 111 | this.event = event; 112 | } 113 | 114 | public NextRTCEvent getEvent() { 115 | return this.event; 116 | } 117 | 118 | public void setEvent(NextRTCEvent event) { 119 | this.event = event; 120 | } 121 | } 122 | 123 | @Component("t3") 124 | @NextRTCEventListener 125 | class T3 implements NextRTCHandler { 126 | 127 | private NextRTCEvent event; 128 | 129 | @Override 130 | public void handleEvent(NextRTCEvent event) { 131 | this.event = event; 132 | } 133 | 134 | public NextRTCEvent getEvent() { 135 | return this.event; 136 | } 137 | 138 | public void setEvent(NextRTCEvent event) { 139 | this.event = event; 140 | } 141 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Signal.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import java.util.Arrays; 4 | import java.util.Optional; 5 | 6 | import static org.apache.commons.lang3.StringUtils.defaultString; 7 | 8 | public class Signal { 9 | public static final Signal EMPTY = new Signal(Signals.EMPTY); 10 | public static final Signal OFFER_REQUEST = new Signal(Signals.OFFER_REQUEST); 11 | public static final Signal OFFER_RESPONSE = new Signal(Signals.OFFER_RESPONSE, Signals.OFFER_RESPONSE_HANDLER); 12 | public static final Signal ANSWER_REQUEST = new Signal(Signals.ANSWER_REQUEST); 13 | public static final Signal ANSWER_RESPONSE = new Signal(Signals.ANSWER_RESPONSE, Signals.ANSWER_RESPONSE_HANDLER); 14 | public static final Signal FINALIZE = new Signal(Signals.FINALIZE); 15 | public static final Signal CANDIDATE = new Signal(Signals.CANDIDATE, Signals.CANDIDATE_HANDLER); 16 | public static final Signal PING = new Signal(Signals.PING); 17 | public static final Signal LEFT = new Signal(Signals.LEFT, Signals.LEFT_HANDLER); 18 | public static final Signal JOIN = new Signal(Signals.JOIN, Signals.JOIN_HANDLER); 19 | public static final Signal CREATE = new Signal(Signals.CREATE, Signals.CREATE_HANDLER); 20 | public static final Signal JOINED = new Signal(Signals.JOINED); 21 | public static final Signal NEW_JOINED = new Signal(Signals.NEW_JOINED); 22 | public static final Signal CREATED = new Signal(Signals.CREATED); 23 | public static final Signal TEXT = new Signal(Signals.TEXT, Signals.TEXT_HANDLER); 24 | public static final Signal ERROR = new Signal(Signals.ERROR); 25 | public static final Signal END = new Signal(Signals.END); 26 | 27 | private static final Signal[] signals = new Signal[]{EMPTY, OFFER_REQUEST, 28 | OFFER_RESPONSE, ANSWER_REQUEST, ANSWER_RESPONSE, FINALIZE, CANDIDATE, 29 | PING, LEFT, JOIN, CREATE, JOINED, NEW_JOINED, CREATED, TEXT, ERROR, END 30 | }; 31 | private final String signalName; 32 | private final String signalHandler; 33 | 34 | Signal(String signalName) { 35 | this(signalName, Signals.EMPTY_HANDLER); 36 | } 37 | 38 | Signal(String signalName, String signalHandler) { 39 | this.signalName = signalName; 40 | this.signalHandler = signalHandler; 41 | } 42 | 43 | public boolean is(String string) { 44 | return ordinaryName().equalsIgnoreCase(string); 45 | } 46 | 47 | public boolean is(Signal signal) { 48 | return this.equals(signal); 49 | } 50 | 51 | public String ordinaryName() { 52 | return signalName; 53 | } 54 | 55 | public static Signal fromString(String string) { 56 | String signalName = defaultString(string); 57 | for (Signal existing : signals) { 58 | if (existing.signalName.equalsIgnoreCase(signalName)) { 59 | return existing; 60 | } 61 | } 62 | return new Signal(signalName); 63 | } 64 | 65 | public static Signal byHandlerName(String string) { 66 | String handlerName = defaultString(string); 67 | Optional signal = Arrays.stream(signals) 68 | .filter(s -> s.signalHandler.equals(handlerName)) 69 | .findFirst(); 70 | if (signal.isPresent()) { 71 | return signal.get(); 72 | } 73 | for (Signal existing : signals) { 74 | if (existing.signalName.equalsIgnoreCase(handlerName)) { 75 | return existing; 76 | } 77 | } 78 | return Signal.EMPTY; 79 | } 80 | 81 | public static Signal[] values() { 82 | return signals; 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return signalName; 88 | } 89 | 90 | @Override 91 | public boolean equals(Object obj) { 92 | if (!(obj instanceof Signal)) { 93 | return false; 94 | } 95 | Signal that = (Signal) obj; 96 | return signalName.equalsIgnoreCase(that.signalName); 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | return signalName.hashCode(); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/EventContentTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.EventChecker; 8 | import org.nextrtc.signalingserver.api.NextRTCEvents; 9 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 10 | import org.nextrtc.signalingserver.repository.Members; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | 14 | import static org.hamcrest.Matchers.is; 15 | import static org.junit.Assert.assertThat; 16 | import static org.junit.Assert.assertTrue; 17 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_CLOSED; 18 | import static org.nextrtc.signalingserver.api.NextRTCEvents.SESSION_OPENED; 19 | 20 | @ContextConfiguration(classes = {SessionOpened.class, SessionClosed.class, UnexpectedSituation.class}) 21 | public class EventContentTest extends BaseTest { 22 | 23 | @Rule 24 | public ExpectedException expect = ExpectedException.none(); 25 | 26 | @Autowired 27 | private Server server; 28 | 29 | @Autowired 30 | private Members members; 31 | 32 | @Autowired 33 | private SessionOpened sessionOpened; 34 | 35 | @Autowired 36 | private SessionClosed sessionClosed; 37 | 38 | @Autowired 39 | private UnexpectedSituation unexpectedSituation; 40 | 41 | @Test 42 | public void shouldPostSessionOpenedCloseAndExceptionEvent() throws Exception { 43 | // given 44 | Connection s1 = mockConnection("s1"); 45 | Connection s2 = mockConnection("s2"); 46 | 47 | // when 48 | server.register(s1); 49 | server.register(s2); 50 | 51 | server.unregister(s1, ""); 52 | server.handleError(s2, new RuntimeException()); 53 | 54 | // then 55 | assertThat(sessionOpened.getEvents().size(), is(2)); 56 | assertTrue(sessionOpened.get(0).from().isPresent()); 57 | assertTrue(sessionOpened.get(1).from().isPresent()); 58 | sessionOpened.get(0).from().ifPresent(from -> 59 | assertThat(from.getId(), is("s1")) 60 | ); 61 | sessionOpened.get(1).from().ifPresent(from -> 62 | assertThat(from.getId(), is("s2")) 63 | ); 64 | 65 | assertThat(sessionClosed.getEvents().size(), is(1)); 66 | assertTrue(sessionClosed.get(0).from().isPresent()); 67 | sessionClosed.get(0).from().ifPresent(from -> 68 | assertThat(from.getId(), is("s1")) 69 | ); 70 | 71 | assertThat(unexpectedSituation.getEvents().size(), is(1)); 72 | assertTrue(unexpectedSituation.get(0).from().isPresent()); 73 | unexpectedSituation.get(0).from().ifPresent(from -> 74 | assertThat(from.getId(), is("s2")) 75 | ); 76 | } 77 | } 78 | 79 | @NextRTCEventListener(SESSION_OPENED) 80 | class SessionOpened extends EventChecker { 81 | 82 | } 83 | 84 | @NextRTCEventListener(SESSION_CLOSED) 85 | class SessionClosed extends EventChecker { 86 | 87 | } 88 | 89 | @NextRTCEventListener(NextRTCEvents.UNEXPECTED_SITUATION) 90 | class UnexpectedSituation extends EventChecker { 91 | 92 | } 93 | 94 | @NextRTCEventListener(NextRTCEvents.CONVERSATION_CREATED) 95 | class ConversationCreated extends EventChecker { 96 | 97 | } 98 | 99 | @NextRTCEventListener(NextRTCEvents.CONVERSATION_DESTROYED) 100 | class ConversationDestroyed extends EventChecker { 101 | 102 | } 103 | 104 | @NextRTCEventListener(NextRTCEvents.MEDIA_LOCAL_STREAM_CREATED) 105 | class LocalStreamCreated extends EventChecker { 106 | 107 | } 108 | 109 | @NextRTCEventListener(NextRTCEvents.MEDIA_LOCAL_STREAM_REQUESTED) 110 | class LocalStreamRequested extends EventChecker { 111 | 112 | } 113 | 114 | @NextRTCEventListener(NextRTCEvents.MEDIA_STREAMING) 115 | class Streaming extends EventChecker { 116 | 117 | } 118 | 119 | @NextRTCEventListener(NextRTCEvents.MEMBER_JOINED) 120 | class MemberJoined extends EventChecker { 121 | 122 | } 123 | 124 | @NextRTCEventListener(NextRTCEvents.MEMBER_LEFT) 125 | class MemberLeft extends EventChecker { 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/MockedClient.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import com.google.common.collect.Lists; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.mockito.ArgumentMatcher; 6 | 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | import java.util.function.Consumer; 12 | 13 | @Slf4j 14 | public class MockedClient extends ArgumentMatcher { 15 | private Server server; 16 | private Connection connection; 17 | private List messages = Lists.newLinkedList(); 18 | private Map candidates = new HashMap<>(); 19 | private Map> behavior = new HashMap<>(); 20 | 21 | { 22 | behavior.put(Signals.CREATED, (message) -> { 23 | }); 24 | behavior.put(Signals.JOINED, (message) -> { 25 | }); 26 | behavior.put(Signals.LEFT, (message) -> { 27 | }); 28 | behavior.put(Signals.TEXT, (message) -> log.info("text message: " + message)); 29 | behavior.put(Signals.END, (message) -> { 30 | }); 31 | behavior.put(Signals.ERROR, (message) -> log.warn("Received error!" + message)); 32 | behavior.put(Signals.NEW_JOINED, (message) -> { 33 | }); 34 | behavior.put(Signals.OFFER_REQUEST, (message) -> server.handle(Message.create() 35 | .to(message.getFrom()) 36 | .signal(Signals.OFFER_RESPONSE) 37 | .content("OFFER: " + connection.getId()) 38 | .build(), connection)); 39 | behavior.put(Signals.ANSWER_REQUEST, (message) -> server.handle(Message.create() 40 | .to(message.getFrom()) 41 | .signal(Signals.ANSWER_RESPONSE) 42 | .content("ANSWER: " + connection.getId()) 43 | .build(), connection)); 44 | behavior.put(Signals.FINALIZE, (message) -> server.handle(Message.create() 45 | .to(message.getFrom()) 46 | .signal(Signals.CANDIDATE) 47 | .content("CANDIDATE: " + connection.getId() + " -> " + message.getFrom()) 48 | .build(), connection)); 49 | behavior.put(Signals.CANDIDATE, (message) -> { 50 | candidates.computeIfAbsent(message.getFrom(), k -> new AtomicInteger()); 51 | AtomicInteger atomicInteger = candidates.get(message.getFrom()); 52 | if (atomicInteger.getAndIncrement() < 1) { 53 | server.handle(Message.create() 54 | .to(message.getFrom()) 55 | .signal(Signals.CANDIDATE) 56 | .content("CANDIDATE: " + connection.getId() + " -> " + message.getFrom()) 57 | .build(), connection); 58 | } 59 | }); 60 | behavior.put("upperCase", (message) -> { 61 | }); 62 | } 63 | 64 | public MockedClient(Server server, Connection connection) { 65 | this.server = server; 66 | this.connection = connection; 67 | } 68 | 69 | @Override 70 | public boolean matches(Object argument) { 71 | if (argument instanceof Message) { 72 | Message msg = (Message) argument; 73 | if (!"ping".equals(msg.getSignal())) { 74 | messages.add(msg); 75 | behavior.get(msg.getSignal()).accept(msg); 76 | } 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | public Message getMessage() { 83 | return messages.get(0); 84 | } 85 | 86 | public Message getMessage(int number) { 87 | if (messages.size() <= number) { 88 | return null; 89 | } 90 | return messages.get(number); 91 | } 92 | 93 | public void reset() { 94 | messages.clear(); 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | StringBuilder sb = new StringBuilder(); 100 | for (Message msg : messages) { 101 | sb.append(msg); 102 | sb.append(", "); 103 | } 104 | return sb.toString(); 105 | } 106 | 107 | public List getMessages() { 108 | return this.messages; 109 | } 110 | } -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.0.0-RC6: 2 | - NextRTCConversation will expose information about creator and members 3 | - Close reason will not be returned in event 4 | - Content in event will not be Optional anymore (you'll get content or empty string) 5 | - Server will be closeable (all thread pools will be closed, and connection released) 6 | v1.0.0-RC5: 7 | - Updated pom and readme 8 | =x=>- Removed dependency to WebSocket JSR-356 9 | - New conversation type MESH_WITH_MASTER (when master leaves then conversation will be destroyed) 10 | v1.0.0-RC4: 11 | - Removed unnecessary log4j.properties file 12 | - Log4j replaced with Slf4j 13 | v1.0.0-RC3: 14 | - Introduced performance tests 15 | - Improved message sender (errors will be send only once whereas normal messages will be retried 3 times) 16 | - In broadcast conversation participant will received newJoined signal with master data 17 | - Ping will not start immediately after member is joined 18 | - When connection is closed message will not be send through it 19 | - Each send will try at least 3 times before failing 20 | ===>- Introduced message sender instead of method send in InternalMessage 21 | - Introduced parallel model inside conversation 22 | - Changed log level 23 | - Synchronization on join signal 24 | - Fixed version of dependencies 25 | - Performance tests 26 | - Used Synchronous communication instead of Asynchronous 27 | - Hanging connections #27 28 | v1.0.0-RC2: 29 | - Changed structure of Exceptions 30 | - Added property nextrtc.join_only_to_existing to decide whether join conversation should thrown an exception when conversation exists or create a new one. 31 | - Changed pom version policy 32 | v1.0.0-RC1: 33 | - Extracted EndpointConfiguration 34 | - Updated README.md 35 | - NextRTC can be run as Spring app or as Standalone server 36 | - Daggered project 37 | - Refactored way of creation beans - Added factories, extracted interfaces and prepared 2 configuration. First for manual - standalone mode, second for Spring mode 38 | - Lomboked again project 39 | - Removed joda-time from dependency (Zoned Date time is used instead) 40 | v0.0.7: 41 | - Fixed problem with flushing server responses 42 | v0.0.6: 43 | - PingTask will not send a messages when connection is closed (now it could sometimes) 44 | - EventDispatcher will catch all Exception, and allow to process rest of handlers (now handlers were executed to the first exception in chain) 45 | - Improved logging for unexpected situation, stacktrace will be in logs in DEBUG level 46 | - One member will be able to create or join to only one conversation 47 | - Candidates will be exchanged even for broadcast conversation 48 | - Added new method to SignalResolver (addCustomHandler) which allows you to add your custom signal 49 | - Added actor test for TEXT message 50 | v0.0.5: 51 | - Added new signal END which is send when broadcaster will exit from broadcast conversation 52 | - Prepared ActorTests where Actor play role of js client 53 | - Added ERROR signal. Improved exception handling. When exception occurs it'll be convert to normal message and connection will not be broken 54 | - Improved logging (Overridden toString) 55 | - CREATE signal will return in custom type of created conversation 56 | - JOIN signal works in the same way as CREATE signal when conversation doesn't not exists 57 | - removed js file from this project (new project for js will be created) 58 | v0.0.4: 59 | - New signal 'newJoined' added to separate two case when someone has joined to conversation and to inform rest of people from conversation about new joiner 60 | - Added js leave method which close websocket connection and stop streaming 61 | - Added default behavior to TEXT message, when message TEXT is sent without recipient, then message will be broadcasted to all conversation members 62 | - Connection context has new feature, ICE candidates can be filter out when they didn't came from broadcaster (master in connection) 63 | - Added broadcast conversation type 64 | - Added policy to handle certain signals 65 | - Expanded API of event new method content has been added (text messages can be stored in java app) 66 | - Added new signal type TEXT (to transfer text messages between communication members) 67 | - Delomboked project - too many questions were connected with building project 68 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/BaseTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver; 2 | 3 | import org.junit.Before; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.ArgumentMatcher; 6 | import org.mockito.Mockito; 7 | import org.nextrtc.signalingserver.cases.CreateConversation; 8 | import org.nextrtc.signalingserver.cases.JoinConversation; 9 | import org.nextrtc.signalingserver.domain.Connection; 10 | import org.nextrtc.signalingserver.domain.InternalMessage; 11 | import org.nextrtc.signalingserver.domain.Member; 12 | import org.nextrtc.signalingserver.domain.Message; 13 | import org.nextrtc.signalingserver.repository.Conversations; 14 | import org.nextrtc.signalingserver.repository.Members; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.context.ApplicationContext; 17 | import org.springframework.test.context.ContextConfiguration; 18 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 19 | 20 | import java.util.List; 21 | import java.util.concurrent.ScheduledFuture; 22 | 23 | import static org.hamcrest.Matchers.is; 24 | import static org.junit.Assert.assertThat; 25 | import static org.mockito.Mockito.*; 26 | 27 | @ContextConfiguration(classes = {TestConfig.class}) 28 | @RunWith(SpringJUnit4ClassRunner.class) 29 | public abstract class BaseTest { 30 | 31 | @Autowired 32 | private CreateConversation create; 33 | 34 | @Autowired 35 | private JoinConversation join; 36 | 37 | @Autowired 38 | private Members members; 39 | 40 | @Autowired 41 | private Conversations conversations; 42 | 43 | @Autowired 44 | private List checkers; 45 | 46 | @Autowired 47 | private ApplicationContext context; 48 | 49 | @Before 50 | public void reset() { 51 | 52 | for (String id : conversations.getAllIds()) { 53 | conversations.remove(id); 54 | } 55 | for (String id : members.getAllIds()) { 56 | members.unregister(id); 57 | } 58 | for (EventChecker checker : checkers) { 59 | checker.reset(); 60 | } 61 | } 62 | 63 | protected Connection mockConnection(String string) { 64 | return mockConnection(string, new MessageMatcher()); 65 | } 66 | 67 | protected Connection mockConnection(String id, ArgumentMatcher match) { 68 | Connection s = mock(Connection.class); 69 | when(s.getId()).thenReturn(id); 70 | when(s.isOpen()).thenReturn(true); 71 | doNothing().when(s).sendObject(Mockito.argThat(match)); 72 | return s; 73 | } 74 | 75 | protected Member mockMember(String string) { 76 | return context.getBean(Member.class, mockConnection(string), mock(ScheduledFuture.class)); 77 | } 78 | 79 | protected Member mockMember(String string, ArgumentMatcher match) { 80 | return context.getBean(Member.class, mockConnection(string, match), mock(ScheduledFuture.class)); 81 | } 82 | 83 | protected void createConversation(String conversationName, Member member) { 84 | members.register(member); 85 | create.execute(InternalMessage.create()// 86 | .from(member)// 87 | .content(conversationName)// 88 | .build()); 89 | } 90 | 91 | protected void createBroadcastConversation(String conversationName, Member member) { 92 | members.register(member); 93 | create.execute(InternalMessage.create()// 94 | .from(member)// 95 | .content(conversationName)// 96 | .addCustom("type", "BROADCAST")// 97 | .build()); 98 | } 99 | 100 | protected void joinConversation(String conversationName, Member member) { 101 | members.register(member); 102 | join.execute(InternalMessage.create()// 103 | .from(member)// 104 | .content(conversationName)// 105 | .build()); 106 | } 107 | 108 | protected void assertMessage(MessageMatcher matcher, int number, String from, String to, String signal, String content) { 109 | assertThat(matcher.getMessage(number).getFrom(), is(from)); 110 | assertThat(matcher.getMessage(number).getTo(), is(to)); 111 | assertThat(matcher.getMessage(number).getSignal(), is(signal)); 112 | assertThat(matcher.getMessage(number).getContent(), is(content)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/conversation/AbstractMeshConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.conversation; 2 | 3 | import com.google.common.collect.Sets; 4 | import org.nextrtc.signalingserver.api.dto.NextRTCMember; 5 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 6 | import org.nextrtc.signalingserver.cases.LeftConversation; 7 | import org.nextrtc.signalingserver.domain.*; 8 | import org.springframework.context.annotation.Scope; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | import java.io.IOException; 13 | import java.util.Set; 14 | 15 | @Component 16 | @Scope("prototype") 17 | public abstract class AbstractMeshConversation extends Conversation { 18 | private ExchangeSignalsBetweenMembers exchange; 19 | 20 | 21 | private Set members = Sets.newConcurrentHashSet(); 22 | private Member creator; 23 | 24 | @Override 25 | public Set getMembers() { 26 | return Sets.newHashSet(members); 27 | } 28 | 29 | @Override 30 | public Member getCreator() { 31 | return creator; 32 | } 33 | 34 | protected Set members() { 35 | return Sets.newHashSet(members); 36 | } 37 | 38 | public AbstractMeshConversation(String id) { 39 | super(id); 40 | } 41 | 42 | public AbstractMeshConversation(String id, 43 | LeftConversation left, 44 | MessageSender sender, 45 | ExchangeSignalsBetweenMembers exchange) { 46 | super(id, left, sender); 47 | this.exchange = exchange; 48 | } 49 | 50 | public abstract String getTypeName(); 51 | 52 | @Override 53 | public synchronized void join(Member sender) { 54 | assignSenderToConversation(sender); 55 | 56 | informSenderThatHasBeenJoined(sender); 57 | 58 | informRestAndBeginSignalExchange(sender); 59 | 60 | members.add(sender); 61 | } 62 | 63 | private void informRestAndBeginSignalExchange(Member sender) { 64 | for (Member to : members) { 65 | parallel.execute(() -> { 66 | sendJoinedFrom(sender, to); 67 | sendJoinedFrom(to, sender); 68 | exchange.begin(to, sender); 69 | }); 70 | } 71 | } 72 | 73 | private void informSenderThatHasBeenJoined(Member sender) { 74 | if (isWithoutMember()) { 75 | this.creator = sender; 76 | sendJoinedToFirst(sender, id); 77 | } else { 78 | sendJoinedToConversation(sender, id); 79 | } 80 | } 81 | 82 | public synchronized boolean isWithoutMember() { 83 | return members.isEmpty(); 84 | } 85 | 86 | public synchronized boolean has(Member member) { 87 | return member != null && members.contains(member); 88 | } 89 | 90 | @Override 91 | public void exchangeSignals(InternalMessage message) { 92 | exchange.execute(message); 93 | } 94 | 95 | @Override 96 | public void broadcast(Member from, InternalMessage message) { 97 | members.stream() 98 | .filter(member -> !member.equals(from)) 99 | .forEach(to -> messageSender.send(message.copy() 100 | .from(from) 101 | .to(to) 102 | .build() 103 | )); 104 | } 105 | 106 | @Override 107 | public synchronized boolean remove(Member leaving) { 108 | boolean remove = members.remove(leaving); 109 | if (remove) { 110 | leaving.unassignConversation(this); 111 | for (Member member : members) { 112 | parallel.execute(() -> sendLeftMessage(leaving, member)); 113 | } 114 | } 115 | if (members.isEmpty()) { 116 | creator = null; 117 | } 118 | return remove; 119 | } 120 | 121 | private void sendJoinedToFirst(Member sender, String id) { 122 | messageSender.send(InternalMessage.create()// 123 | .to(sender)// 124 | .signal(Signal.CREATED)// 125 | .addCustom("type", "MESH") 126 | .content(id)// 127 | .build()// 128 | ); 129 | } 130 | 131 | @Override 132 | public void close() throws IOException { 133 | members.parallelStream().forEach(this::remove); 134 | } 135 | 136 | @Inject 137 | public void setExchange(ExchangeSignalsBetweenMembers exchange) { 138 | this.exchange = exchange; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/EventContext.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import com.google.common.collect.Maps; 4 | import org.nextrtc.signalingserver.api.NextRTCEvents; 5 | import org.nextrtc.signalingserver.api.dto.NextRTCConversation; 6 | import org.nextrtc.signalingserver.api.dto.NextRTCEvent; 7 | import org.nextrtc.signalingserver.api.dto.NextRTCMember; 8 | import org.nextrtc.signalingserver.exception.SignalingException; 9 | 10 | import java.time.ZonedDateTime; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | 14 | import static java.util.Collections.unmodifiableMap; 15 | import static java.util.Optional.ofNullable; 16 | import static org.apache.commons.lang3.StringUtils.defaultString; 17 | 18 | public class EventContext implements NextRTCEvent { 19 | 20 | private final NextRTCEvents type; 21 | private final ZonedDateTime published = ZonedDateTime.now(); 22 | private final Map custom = Maps.newHashMap(); 23 | private final NextRTCMember from; 24 | private final NextRTCMember to; 25 | private final NextRTCConversation conversation; 26 | private final SignalingException exception; 27 | private final String content; 28 | 29 | private EventContext(NextRTCEvents type, NextRTCMember from, NextRTCMember to, NextRTCConversation conversation, SignalingException exception, String content) { 30 | this.type = type; 31 | this.from = from; 32 | this.to = to; 33 | this.conversation = conversation; 34 | this.exception = exception; 35 | this.content = content; 36 | } 37 | 38 | @Override 39 | public NextRTCEvents type() { 40 | return type; 41 | } 42 | 43 | @Override 44 | public ZonedDateTime published() { 45 | return published; 46 | } 47 | 48 | @Override 49 | public Optional from() { 50 | return ofNullable(from); 51 | } 52 | 53 | @Override 54 | public Optional to() { 55 | return ofNullable(to); 56 | } 57 | 58 | @Override 59 | public Optional conversation() { 60 | return ofNullable(conversation); 61 | } 62 | 63 | @Override 64 | public Optional exception() { 65 | return ofNullable(exception); 66 | } 67 | 68 | @Override 69 | public Map custom() { 70 | return unmodifiableMap(custom); 71 | } 72 | 73 | @Override 74 | public String content() { 75 | return defaultString(content); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return String.format("%s (%s -> %s) | conv: %s | %s", type, from, to, conversation, custom); 81 | } 82 | 83 | public static EventContextBuilder builder() { 84 | return new EventContextBuilder(); 85 | } 86 | 87 | public static class EventContextBuilder { 88 | private Map custom; 89 | private NextRTCEvents type; 90 | private NextRTCMember from; 91 | private NextRTCMember to; 92 | private NextRTCConversation conversation; 93 | private SignalingException exception; 94 | private String content; 95 | 96 | public EventContextBuilder content(String content) { 97 | this.content = content; 98 | return this; 99 | } 100 | 101 | public EventContextBuilder type(NextRTCEvents type) { 102 | this.type = type; 103 | return this; 104 | } 105 | 106 | public EventContextBuilder custom(Map custom) { 107 | this.custom = custom; 108 | return this; 109 | } 110 | 111 | public EventContextBuilder from(NextRTCMember from) { 112 | this.from = from; 113 | return this; 114 | } 115 | 116 | public EventContextBuilder to(NextRTCMember to) { 117 | this.to = to; 118 | return this; 119 | } 120 | 121 | public EventContextBuilder conversation(NextRTCConversation conversation) { 122 | this.conversation = conversation; 123 | return this; 124 | } 125 | 126 | public EventContextBuilder exception(SignalingException exception) { 127 | this.exception = exception; 128 | return this; 129 | } 130 | 131 | public NextRTCEvent build() { 132 | if (type == null) { 133 | throw new IllegalArgumentException("Type is required"); 134 | } 135 | EventContext eventContext = new EventContext(type, from, to, conversation, exception, content); 136 | if (custom != null) { 137 | eventContext.custom.putAll(custom); 138 | } 139 | return eventContext; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/domain/conversation/BroadcastConversationTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.conversation; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.MessageMatcher; 6 | import org.nextrtc.signalingserver.cases.LeftConversation; 7 | import org.nextrtc.signalingserver.domain.Conversation; 8 | import org.nextrtc.signalingserver.domain.InternalMessage; 9 | import org.nextrtc.signalingserver.domain.Member; 10 | import org.nextrtc.signalingserver.repository.Conversations; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.junit.Assert.assertFalse; 15 | import static org.junit.Assert.assertThat; 16 | 17 | public class BroadcastConversationTest extends BaseTest { 18 | 19 | @Autowired 20 | private Conversations conversations; 21 | 22 | @Autowired 23 | private LeftConversation leftConversation; 24 | 25 | @Test 26 | public void shouldJoinBroadcaster() { 27 | // given 28 | MessageMatcher match = new MessageMatcher(); 29 | Member broadcaster = mockMember("John", match); 30 | 31 | // when 32 | createBroadcastConversation("broadcastId", broadcaster); 33 | 34 | // then 35 | assertThat(match.getMessage().getSignal(), is("created")); 36 | assertThat(match.getMessage().getContent(), is("broadcastId")); 37 | Conversation conversation = conversations.findBy("broadcastId").get(); 38 | assertThat(conversation.isWithoutMember(), is(false)); 39 | assertThat(conversation.has(broadcaster), is(true)); 40 | } 41 | 42 | @Test 43 | public void shouldJoinBroadcasterAudience() { 44 | // given 45 | MessageMatcher match = new MessageMatcher(); 46 | Member broadcaster = mockMember("John", match); 47 | MessageMatcher audienceMatch = new MessageMatcher(); 48 | Member audience = mockMember("Audience", audienceMatch); 49 | createBroadcastConversation("broadcastId", broadcaster); 50 | 51 | // when 52 | joinConversation("broadcastId", audience); 53 | 54 | // then 55 | assertThat(match.getMessage(0).getSignal(), is("created")); 56 | assertThat(match.getMessage(0).getContent(), is("broadcastId")); 57 | assertThat(audienceMatch.getMessage(0).getSignal(), is("joined")); 58 | assertThat(audienceMatch.getMessage(0).getContent(), is("broadcastId")); 59 | Conversation conversation = conversations.findBy("broadcastId").get(); 60 | assertThat(conversation.isWithoutMember(), is(false)); 61 | assertThat(conversation.has(broadcaster), is(true)); 62 | assertThat(conversation.has(audience), is(true)); 63 | } 64 | 65 | @Test 66 | public void shouldAllowToLeftAudienceWithInfoToBroadcaster() { 67 | // given 68 | MessageMatcher match = new MessageMatcher(); 69 | Member broadcaster = mockMember("John", match); 70 | MessageMatcher audienceMatch = new MessageMatcher(); 71 | Member audience = mockMember("Audience", audienceMatch); 72 | createBroadcastConversation("broadcastId", broadcaster); 73 | joinConversation("broadcastId", audience); 74 | match.reset(); 75 | audienceMatch.reset(); 76 | 77 | // when 78 | leftConversation.execute(InternalMessage.create() 79 | .from(audience) 80 | .build()); 81 | 82 | // then 83 | assertThat(match.getMessage(0).getSignal(), is("left")); 84 | assertThat(match.getMessage(0).getFrom(), is(audience.getId())); 85 | Conversation conversation = conversations.findBy("broadcastId").get(); 86 | assertThat(conversation.isWithoutMember(), is(false)); 87 | assertThat(conversation.has(broadcaster), is(true)); 88 | assertThat(conversation.has(audience), is(false)); 89 | } 90 | 91 | @Test 92 | public void shouldThrowOutConversationWhenBroadcasterLeft() { 93 | // given 94 | Member broadcaster = mockMember("John"); 95 | MessageMatcher audienceMatch = new MessageMatcher(); 96 | Member audience = mockMember("Audience", audienceMatch); 97 | createBroadcastConversation("broadcastId", broadcaster); 98 | joinConversation("broadcastId", audience); 99 | audienceMatch.reset(); 100 | 101 | // when 102 | leftConversation.execute(InternalMessage.create() 103 | .from(broadcaster) 104 | .build()); 105 | 106 | // then 107 | assertThat(audienceMatch.getMessage(0).getSignal(), is("left")); 108 | assertThat(audienceMatch.getMessage(0).getFrom(), is(broadcaster.getId())); 109 | assertFalse(conversations.findBy("broadcastId").isPresent()); 110 | } 111 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/cases/connection/ConnectionContext.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases.connection; 2 | 3 | import lombok.Getter; 4 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 5 | import org.nextrtc.signalingserver.api.NextRTCEvents; 6 | import org.nextrtc.signalingserver.domain.InternalMessage; 7 | import org.nextrtc.signalingserver.domain.Member; 8 | import org.nextrtc.signalingserver.domain.MessageSender; 9 | import org.nextrtc.signalingserver.domain.Signal; 10 | import org.nextrtc.signalingserver.property.NextRTCProperties; 11 | import org.springframework.context.annotation.Scope; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.inject.Inject; 15 | import java.time.ZonedDateTime; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | @Getter 20 | @Component 21 | @Scope("prototype") 22 | public class ConnectionContext { 23 | 24 | private ConnectionState state = ConnectionState.NOT_INITIALIZED; 25 | private ZonedDateTime lastUpdated = ZonedDateTime.now(); 26 | 27 | private NextRTCProperties properties; 28 | private NextRTCEventBus bus; 29 | private MessageSender sender; 30 | 31 | private Member master; 32 | private Member slave; 33 | private List candidates = new ArrayList<>(); 34 | 35 | public ConnectionContext(Member master, Member slave) { 36 | this.master = master; 37 | this.slave = slave; 38 | } 39 | 40 | 41 | public void process(InternalMessage message) { 42 | if(message.getSignal() == Signal.CANDIDATE && !is(message, ConnectionState.EXCHANGE_CANDIDATES)){ 43 | recordCandidate(message); 44 | } 45 | if (is(message, ConnectionState.OFFER_REQUESTED)) { 46 | setState(ConnectionState.ANSWER_REQUESTED); 47 | answerRequest(message); 48 | } else if (is(message, ConnectionState.ANSWER_REQUESTED)) { 49 | setState(ConnectionState.EXCHANGE_CANDIDATES); 50 | finalize(message); 51 | sendCollectedCandidates(); 52 | } else if (is(message, ConnectionState.EXCHANGE_CANDIDATES)) { 53 | exchangeCandidates(message); 54 | } 55 | } 56 | 57 | private void sendCollectedCandidates() { 58 | candidates.forEach(this::exchangeCandidates); 59 | candidates.clear(); 60 | } 61 | 62 | private void recordCandidate(InternalMessage message) { 63 | candidates.add(message); 64 | } 65 | 66 | 67 | private void exchangeCandidates(InternalMessage message) { 68 | sender.send(message.copy() 69 | .signal(Signal.CANDIDATE) 70 | .build() 71 | ); 72 | } 73 | 74 | 75 | private void finalize(InternalMessage message) { 76 | sender.send(message.copy()// 77 | .from(slave)// 78 | .to(master)// 79 | .signal(Signal.FINALIZE)// 80 | .build()); 81 | bus.post(NextRTCEvents.MEDIA_LOCAL_STREAM_CREATED.occurFor(slave.getConnection())); 82 | bus.post(NextRTCEvents.MEDIA_STREAMING.occurFor(master.getConnection())); 83 | bus.post(NextRTCEvents.MEDIA_STREAMING.occurFor(slave.getConnection())); 84 | } 85 | 86 | 87 | private void answerRequest(InternalMessage message) { 88 | bus.post(NextRTCEvents.MEDIA_LOCAL_STREAM_CREATED.occurFor(master.getConnection())); 89 | sender.send(message.copy()// 90 | .from(master)// 91 | .to(slave)// 92 | .signal(Signal.ANSWER_REQUEST)// 93 | .build()// 94 | ); 95 | bus.post(NextRTCEvents.MEDIA_LOCAL_STREAM_REQUESTED.occurFor(slave.getConnection())); 96 | } 97 | 98 | private boolean is(InternalMessage message, ConnectionState state) { 99 | return state.equals(this.state) && state.isValid(message); 100 | } 101 | 102 | public void begin() { 103 | setState(ConnectionState.OFFER_REQUESTED); 104 | sender.send(InternalMessage.create()// 105 | .from(slave)// 106 | .to(master)// 107 | .signal(Signal.OFFER_REQUEST) 108 | .build()// 109 | ); 110 | bus.post(NextRTCEvents.MEDIA_LOCAL_STREAM_REQUESTED.occurFor(master.getConnection())); 111 | } 112 | 113 | public boolean isCurrent() { 114 | return lastUpdated.plusSeconds(properties.getMaxConnectionSetupTime()).isAfter(ZonedDateTime.now()); 115 | } 116 | 117 | private void setState(ConnectionState state) { 118 | this.state = state; 119 | lastUpdated = ZonedDateTime.now(); 120 | } 121 | 122 | @Inject 123 | public void setBus(NextRTCEventBus bus) { 124 | this.bus = bus; 125 | } 126 | 127 | @Inject 128 | public void setProperties(NextRTCProperties properties) { 129 | this.properties = properties; 130 | } 131 | 132 | @Inject 133 | public void setSender(MessageSender sender) { 134 | this.sender = sender; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/TextMessageTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Test; 4 | import org.nextrtc.signalingserver.BaseTest; 5 | import org.nextrtc.signalingserver.MessageMatcher; 6 | import org.nextrtc.signalingserver.domain.Member; 7 | import org.nextrtc.signalingserver.domain.Signal; 8 | import org.nextrtc.signalingserver.repository.Members; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import static org.awaitility.Awaitility.await; 12 | import static org.hamcrest.Matchers.hasSize; 13 | import static org.hamcrest.Matchers.is; 14 | import static org.junit.Assert.assertThat; 15 | import static org.nextrtc.signalingserver.domain.InternalMessage.create; 16 | 17 | public class TextMessageTest extends BaseTest { 18 | 19 | @Autowired 20 | private TextMessage textMessage; 21 | 22 | @Autowired 23 | private Members members; 24 | 25 | @Test 26 | public void shouldSendMessageFromOneToAnother() throws Exception { 27 | // given 28 | MessageMatcher johnMatcher = new MessageMatcher(); 29 | MessageMatcher stanMatcher = new MessageMatcher(); 30 | Member john = mockMember("Jan", johnMatcher); 31 | Member stan = mockMember("Stefan", stanMatcher); 32 | members.register(john); 33 | members.register(stan); 34 | createConversation("c", john); 35 | await().until(() -> johnMatcher.has(m -> m.getSignal().equals("created")).check()); 36 | joinConversation("c", stan); 37 | await().until(() -> johnMatcher.has(m -> m.getSignal().equals("newJoined")).check()); 38 | johnMatcher.reset(); 39 | stanMatcher.reset(); 40 | 41 | // when 42 | textMessage.execute(create() 43 | .from(john) 44 | .to(stan) 45 | .signal(Signal.TEXT) 46 | .content("Hello!") 47 | .addCustom("type", "Greeting") 48 | .build()); 49 | 50 | // then 51 | assertThat(johnMatcher.getMessages(), hasSize(0)); 52 | assertThat(stanMatcher.getMessages(), hasSize(1)); 53 | assertThat(stanMatcher.getMessage().getContent(), is("Hello!")); 54 | assertThat(stanMatcher.getMessage().getFrom(), is("Jan")); 55 | assertThat(stanMatcher.getMessage().getTo(), is("Stefan")); 56 | assertThat(stanMatcher.getMessage().getSignal(), is("text")); 57 | assertThat(stanMatcher.getMessage().getCustom().get("type"), is("Greeting")); 58 | } 59 | 60 | @Test 61 | public void shouldSendMessageToAllMemberOfConversationIfToIsEmpty() throws Exception { 62 | // given 63 | MessageMatcher johnMatcher = new MessageMatcher(); 64 | MessageMatcher stanMatcher = new MessageMatcher(); 65 | MessageMatcher markMatcher = new MessageMatcher(); 66 | Member john = mockMember("Jan", johnMatcher); 67 | Member stan = mockMember("Stefan", stanMatcher); 68 | Member mark = mockMember("Marek", markMatcher); 69 | members.register(john); 70 | members.register(stan); 71 | members.register(mark); 72 | createConversation("c", john); 73 | await().until(() -> johnMatcher.has(m -> m.getSignal().equals("created")).check()); 74 | joinConversation("c", stan); 75 | joinConversation("c", mark); 76 | await().until(() -> stanMatcher.has(m -> m.getSignal().equals("newJoined")).check()); 77 | await().until(() -> markMatcher.has(m -> m.getSignal().equals("newJoined")).check()); 78 | johnMatcher.reset(); 79 | stanMatcher.reset(); 80 | markMatcher.reset(); 81 | 82 | // when 83 | textMessage.execute(create() 84 | .from(john) 85 | .signal(Signal.TEXT) 86 | .content("Hello!") 87 | .addCustom("type", "Greeting") 88 | .build()); 89 | 90 | // then 91 | assertThat(johnMatcher.getMessages(), hasSize(0)); 92 | assertThat(stanMatcher.getMessages(), hasSize(1)); 93 | assertThat(markMatcher.getMessages(), hasSize(1)); 94 | assertMessage(stanMatcher, 0, "Jan", "Stefan", "text", "Hello!"); 95 | assertThat(stanMatcher.getMessage().getCustom().get("type"), is("Greeting")); 96 | assertMessage(markMatcher, 0, "Jan", "Marek", "text", "Hello!"); 97 | assertThat(stanMatcher.getMessage().getCustom().get("type"), is("Greeting")); 98 | 99 | } 100 | 101 | @Test 102 | public void shouldSendMessageFromOneToAnotherBuInSameConversation() throws Exception { 103 | // given 104 | MessageMatcher johnMatcher = new MessageMatcher(); 105 | MessageMatcher stanMatcher = new MessageMatcher(); 106 | Member john = mockMember("Jan", johnMatcher); 107 | Member stan = mockMember("Stefan", stanMatcher); 108 | members.register(john); 109 | members.register(stan); 110 | createConversation("d", john); 111 | johnMatcher.reset(); 112 | 113 | // when 114 | textMessage.execute(create() 115 | .from(john) 116 | .to(stan) 117 | .signal(Signal.TEXT) 118 | .content("Hello!") 119 | .addCustom("type", "Greeting") 120 | .build()); 121 | 122 | // then 123 | assertThat(johnMatcher.getMessages(), hasSize(0)); 124 | assertThat(stanMatcher.getMessages(), hasSize(0)); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/conversation/BroadcastConversation.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain.conversation; 2 | 3 | import com.google.common.collect.Sets; 4 | import org.nextrtc.signalingserver.api.dto.NextRTCMember; 5 | import org.nextrtc.signalingserver.cases.ExchangeSignalsBetweenMembers; 6 | import org.nextrtc.signalingserver.cases.LeftConversation; 7 | import org.nextrtc.signalingserver.domain.*; 8 | import org.springframework.context.annotation.Scope; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.inject.Inject; 12 | import java.io.IOException; 13 | import java.util.Set; 14 | 15 | @Component 16 | @Scope("prototype") 17 | public class BroadcastConversation extends Conversation { 18 | 19 | private ExchangeSignalsBetweenMembers exchange; 20 | private Member broadcaster; 21 | private Set audience = Sets.newConcurrentHashSet(); 22 | 23 | public BroadcastConversation(String id) { 24 | super(id); 25 | } 26 | 27 | public BroadcastConversation(String id, 28 | LeftConversation left, 29 | MessageSender sender, 30 | ExchangeSignalsBetweenMembers exchange) { 31 | super(id, left, sender); 32 | this.exchange = exchange; 33 | } 34 | 35 | @Override 36 | public synchronized void join(Member sender) { 37 | assignSenderToConversation(sender); 38 | 39 | informSenderThatHasBeenJoined(sender); 40 | 41 | beginSignalExchangeBetweenBroadcasterAndNewAudience(sender); 42 | 43 | if (!broadcaster.equals(sender)) { 44 | audience.add(sender); 45 | } 46 | } 47 | 48 | @Override 49 | public synchronized boolean remove(Member leaving) { 50 | if (broadcaster.equals(leaving)) { 51 | for (Member member : audience) { 52 | sendLeftMessage(broadcaster, member); 53 | sendEndMessage(broadcaster, member); 54 | member.unassignConversation(this); 55 | } 56 | audience.clear(); 57 | broadcaster.unassignConversation(this); 58 | broadcaster = null; 59 | return true; 60 | } 61 | sendLeftMessage(leaving, broadcaster); 62 | boolean remove = audience.remove(leaving); 63 | if (remove) { 64 | leaving.unassignConversation(this); 65 | } 66 | return remove; 67 | } 68 | 69 | private void sendEndMessage(Member leaving, Member recipient) { 70 | messageSender.send(InternalMessage.create()// 71 | .from(leaving)// 72 | .to(recipient)// 73 | .signal(Signal.END)// 74 | .content(id)// 75 | .build()// 76 | ); 77 | } 78 | 79 | @Override 80 | public synchronized boolean isWithoutMember() { 81 | return broadcaster == null && audience.isEmpty(); 82 | } 83 | 84 | @Override 85 | public synchronized boolean has(Member from) { 86 | return broadcaster != null 87 | && (broadcaster.equals(from) || audience.contains(from)); 88 | } 89 | 90 | @Override 91 | public void exchangeSignals(InternalMessage message) { 92 | exchange.execute(message); 93 | } 94 | 95 | @Override 96 | public void broadcast(Member from, InternalMessage message) { 97 | audience.stream() 98 | .filter(member -> !member.equals(from)) 99 | .forEach(to -> messageSender.send(message.copy() 100 | .from(from) 101 | .to(to) 102 | .build() 103 | )); 104 | if (from != broadcaster) { 105 | messageSender.send(message.copy() 106 | .from(from) 107 | .to(broadcaster) 108 | .build() 109 | ); 110 | } 111 | } 112 | 113 | private void informSenderThatHasBeenJoined(Member sender) { 114 | if (isWithoutMember()) { 115 | broadcaster = sender; 116 | sendJoinedToBroadcaster(sender, id); 117 | } else { 118 | sendJoinedToConversation(sender, id); 119 | sendJoinedFrom(broadcaster, sender); 120 | } 121 | } 122 | 123 | private void beginSignalExchangeBetweenBroadcasterAndNewAudience(Member sender) { 124 | if (!sender.equals(broadcaster)) { 125 | sendJoinedFrom(sender, broadcaster); 126 | exchange.begin(broadcaster, sender); 127 | } 128 | } 129 | 130 | private void sendJoinedToBroadcaster(Member sender, String id) { 131 | messageSender.send(InternalMessage.create()// 132 | .to(sender)// 133 | .signal(Signal.CREATED)// 134 | .addCustom("type", "BROADCAST") 135 | .content(id)// 136 | .build()// 137 | ); 138 | } 139 | 140 | @Override 141 | public void close() throws IOException { 142 | remove(broadcaster); 143 | } 144 | 145 | @Inject 146 | public void setExchange(ExchangeSignalsBetweenMembers exchange) { 147 | this.exchange = exchange; 148 | } 149 | 150 | @Override 151 | public Member getCreator() { 152 | return broadcaster; 153 | } 154 | 155 | @Override 156 | public Set getMembers() { 157 | return Sets.newHashSet(audience); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/org/nextrtc/signalingserver/domain/Server.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.domain; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.lang3.tuple.Pair; 5 | import org.nextrtc.signalingserver.api.NextRTCEventBus; 6 | import org.nextrtc.signalingserver.api.NextRTCServer; 7 | import org.nextrtc.signalingserver.cases.RegisterMember; 8 | import org.nextrtc.signalingserver.cases.SignalHandler; 9 | import org.nextrtc.signalingserver.domain.InternalMessage.InternalMessageBuilder; 10 | import org.nextrtc.signalingserver.exception.SignalingException; 11 | import org.nextrtc.signalingserver.repository.MemberRepository; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.inject.Inject; 15 | import java.io.IOException; 16 | import java.io.PrintWriter; 17 | import java.io.StringWriter; 18 | import java.util.function.Consumer; 19 | 20 | import static org.nextrtc.signalingserver.api.NextRTCEvents.*; 21 | import static org.nextrtc.signalingserver.exception.Exceptions.MEMBER_NOT_FOUND; 22 | 23 | @Slf4j 24 | @Component 25 | public class Server implements NextRTCServer { 26 | 27 | private final CloseableContext context; 28 | private final NextRTCEventBus eventBus; 29 | private final MemberRepository members; 30 | private final SignalResolver resolver; 31 | private final RegisterMember register; 32 | private final MessageSender sender; 33 | 34 | @Inject 35 | public Server(NextRTCEventBus eventBus, 36 | MemberRepository members, 37 | SignalResolver resolver, 38 | RegisterMember register, 39 | MessageSender sender, 40 | CloseableContext context) { 41 | this.eventBus = eventBus; 42 | this.members = members; 43 | this.resolver = resolver; 44 | this.register = register; 45 | this.sender = sender; 46 | this.context = context; 47 | } 48 | 49 | public void register(Connection s) { 50 | doSaveExecution(s, session -> 51 | register.incoming(session) 52 | ); 53 | } 54 | 55 | public void handle(Message external, Connection s) { 56 | doSaveExecution(s, session -> { 57 | Pair resolve = resolver.resolve(external.getSignal()); 58 | InternalMessage internalMessage = buildInternalMessage(external, resolve.getKey(), session); 59 | eventBus.post(MESSAGE.basedOn(internalMessage)); 60 | processMessage(resolve.getValue(), internalMessage); 61 | }); 62 | } 63 | 64 | private void processMessage(SignalHandler handler, InternalMessage message) { 65 | log.debug("Incoming: " + message); 66 | if (handler != null) { 67 | handler.execute(message); 68 | } 69 | } 70 | 71 | private InternalMessage buildInternalMessage(Message message, Signal signal, Connection connection) { 72 | InternalMessageBuilder bld = InternalMessage.create()// 73 | .from(findMember(connection))// 74 | .content(message.getContent())// 75 | .signal(signal)// 76 | .custom(message.getCustom()); 77 | members.findBy(message.getTo()).ifPresent(bld::to); 78 | return bld.build(); 79 | } 80 | 81 | private Member findMember(Connection connection) { 82 | return members.findBy(connection.getId()).orElseThrow(() -> new SignalingException(MEMBER_NOT_FOUND)); 83 | } 84 | 85 | public void unregister(Connection connection, String reason) { 86 | doSaveExecution(connection, session -> { 87 | members.unregister(session.getId()); 88 | eventBus.post(SESSION_CLOSED.occurFor(session, reason)); 89 | } 90 | ); 91 | } 92 | 93 | 94 | public void handleError(Connection connection, Throwable exception) { 95 | doSaveExecution(connection, session -> { 96 | members.unregister(session.getId()); 97 | eventBus.post(UNEXPECTED_SITUATION.occurFor(session, exception.getMessage())); 98 | } 99 | ); 100 | } 101 | 102 | private void doSaveExecution(Connection connection, Consumer action) { 103 | try { 104 | action.accept(connection); 105 | } catch (Exception e) { 106 | log.warn("Server will try to handle this exception and send information as normal message through websocket", e); 107 | sendErrorOverWebSocket(connection, e); 108 | } 109 | } 110 | 111 | private void sendErrorOverWebSocket(Connection session, Exception e) { 112 | try { 113 | sender.send(InternalMessage.create() 114 | .to(new Member(session, null)) 115 | .signal(Signal.ERROR) 116 | .content(e.getMessage()) 117 | .addCustom("stackTrace", writeStackTraceToString(e)) 118 | .build() 119 | ); 120 | } catch (Exception resendException) { 121 | log.error("Something goes wrong during resend! Exception omitted", resendException); 122 | } 123 | } 124 | 125 | private String writeStackTraceToString(Exception e) { 126 | if (log.isDebugEnabled()) { 127 | StringWriter errors = new StringWriter(); 128 | e.printStackTrace(new PrintWriter(errors)); 129 | return errors.toString(); 130 | } 131 | return e.getClass().getSimpleName() + " - " + e.getMessage(); 132 | } 133 | 134 | @Override 135 | public void close() throws IOException { 136 | context.close(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/JoinConversationTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.MessageMatcher; 8 | import org.nextrtc.signalingserver.domain.InternalMessage; 9 | import org.nextrtc.signalingserver.domain.Member; 10 | import org.nextrtc.signalingserver.exception.SignalingException; 11 | import org.nextrtc.signalingserver.property.NextRTCProperties; 12 | import org.nextrtc.signalingserver.repository.ConversationRepository; 13 | import org.nextrtc.signalingserver.repository.Members; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | import static junit.framework.TestCase.assertTrue; 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.when; 21 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NOT_FOUND; 22 | import static org.nextrtc.signalingserver.exception.Exceptions.MEMBER_IN_OTHER_CONVERSATION; 23 | 24 | public class JoinConversationTest extends BaseTest { 25 | 26 | @Autowired 27 | private JoinConversation joinConversation; 28 | 29 | @Autowired 30 | private Members members; 31 | 32 | @Autowired 33 | private ConversationRepository conversations; 34 | 35 | @Rule 36 | public ExpectedException exception = ExpectedException.none(); 37 | 38 | @Test 39 | public void shouldCreateNewConversationIfConversationDoesntExists() throws Exception { 40 | // given 41 | MessageMatcher match = new MessageMatcher(); 42 | Member member = mockMember("Jan", match); 43 | members.register(member); 44 | 45 | // when 46 | joinConversation.execute(InternalMessage.create()// 47 | .from(member)// 48 | .content("new conversation")// 49 | .build()); 50 | 51 | // then 52 | assertThat(match.getMessage().getSignal(), is("created")); 53 | assertThat(match.getMessage().getTo(), is("Jan")); 54 | assertThat(match.getMessage().getContent(), is("new conversation")); 55 | assertThat(match.getMessage().getCustom().size(), is(1)); 56 | assertThat(match.getMessage().getCustom().get("type"), is("MESH")); 57 | } 58 | 59 | @Test 60 | public void shouldCreateNewConversationIfConversationDoesntExists_andHandleType() throws Exception { 61 | // given 62 | MessageMatcher match = new MessageMatcher(); 63 | Member member = mockMember("Jan", match); 64 | members.register(member); 65 | 66 | // when 67 | joinConversation.execute(InternalMessage.create()// 68 | .from(member)// 69 | .content("new conversation")// 70 | .addCustom("type", "BROADCAST") 71 | .build()); 72 | 73 | // then 74 | assertThat(match.getMessage().getSignal(), is("created")); 75 | assertThat(match.getMessage().getTo(), is("Jan")); 76 | assertThat(match.getMessage().getContent(), is("new conversation")); 77 | assertThat(match.getMessage().getCustom().size(), is(1)); 78 | assertThat(match.getMessage().getCustom().get("type"), is("BROADCAST")); 79 | } 80 | 81 | @Test 82 | public void shouldJoinMemberToConversation() throws Exception { 83 | // given 84 | Member member = mockMember("Jan"); 85 | members.register(member); 86 | Member stach = mockMember("Stach"); 87 | members.register(stach); 88 | createConversation("conv", stach); 89 | 90 | // when 91 | joinConversation.execute(InternalMessage.create()// 92 | .from(member)// 93 | .content("conv")// 94 | .build()); 95 | 96 | // then 97 | assertTrue(member.getConversation().isPresent()); 98 | } 99 | 100 | @Test 101 | public void shouldThrowExceptionWhenUserIsInOtherConversation() throws Exception { 102 | // given 103 | Member jan = mockMember("Jan"); 104 | members.register(jan); 105 | Member stach = mockMember("Stach"); 106 | members.register(stach); 107 | Member stefan = mockMember("Stefan"); 108 | members.register(stefan); 109 | createConversation("conv", stach); 110 | createConversation("conv2", stefan); 111 | joinConversation.execute(InternalMessage.create()// 112 | .from(jan)// 113 | .content("conv2")// 114 | .build()); 115 | 116 | // then 117 | exception.expect(SignalingException.class); 118 | exception.expectMessage(MEMBER_IN_OTHER_CONVERSATION.name()); 119 | 120 | // when 121 | joinConversation.execute(InternalMessage.create()// 122 | .from(jan)// 123 | .content("conv")// 124 | .build()); 125 | } 126 | 127 | @Test 128 | public void shouldThrownAnExceptionWhenJoinToExistingConversationIsSetToTrueAndConversationDoesntExist() throws Exception { 129 | // given 130 | NextRTCProperties properties = mock(NextRTCProperties.class); 131 | when(properties.isJoinOnlyToExisting()).thenReturn(true); 132 | Member jan = mockMember("Jan"); 133 | members.register(jan); 134 | 135 | // then 136 | exception.expect(SignalingException.class); 137 | exception.expectMessage(CONVERSATION_NOT_FOUND.name()); 138 | 139 | // when 140 | new JoinConversation(conversations, null, properties).execute(InternalMessage.create()// 141 | .from(jan)// 142 | .content("conv")// 143 | .build()); 144 | } 145 | 146 | 147 | 148 | } -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/cases/CreateConversationTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.cases; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | import org.nextrtc.signalingserver.BaseTest; 7 | import org.nextrtc.signalingserver.EventChecker; 8 | import org.nextrtc.signalingserver.MessageMatcher; 9 | import org.nextrtc.signalingserver.api.NextRTCEvents; 10 | import org.nextrtc.signalingserver.api.annotation.NextRTCEventListener; 11 | import org.nextrtc.signalingserver.domain.Conversation; 12 | import org.nextrtc.signalingserver.domain.InternalMessage; 13 | import org.nextrtc.signalingserver.domain.Member; 14 | import org.nextrtc.signalingserver.domain.ServerEventCheck; 15 | import org.nextrtc.signalingserver.exception.SignalingException; 16 | import org.nextrtc.signalingserver.repository.Conversations; 17 | import org.nextrtc.signalingserver.repository.Members; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.test.context.ContextConfiguration; 21 | 22 | import java.util.Optional; 23 | 24 | import static org.hamcrest.Matchers.is; 25 | import static org.junit.Assert.assertThat; 26 | import static org.nextrtc.signalingserver.api.NextRTCEvents.CONVERSATION_CREATED; 27 | import static org.nextrtc.signalingserver.exception.Exceptions.CONVERSATION_NAME_OCCUPIED; 28 | import static org.nextrtc.signalingserver.exception.Exceptions.MEMBER_IN_OTHER_CONVERSATION; 29 | 30 | @ContextConfiguration(classes = ServerEventCheck.class) 31 | public class CreateConversationTest extends BaseTest { 32 | 33 | @Component 34 | @NextRTCEventListener(CONVERSATION_CREATED) 35 | public static class ServerEventCheck extends EventChecker { 36 | 37 | } 38 | 39 | @Autowired 40 | private CreateConversation create; 41 | 42 | @Autowired 43 | private Conversations conversations; 44 | 45 | @Autowired 46 | private Members members; 47 | 48 | @Rule 49 | public ExpectedException exception = ExpectedException.none(); 50 | 51 | @Autowired 52 | private ServerEventCheck eventCall; 53 | 54 | @Test 55 | public void shouldCreateConversation() throws Exception { 56 | // given 57 | MessageMatcher match = new MessageMatcher(); 58 | Member member = mockMember("Jan", match); 59 | members.register(member); 60 | 61 | // when 62 | create.execute(InternalMessage.create()// 63 | .from(member)// 64 | .content("new conversation")// 65 | .build()); 66 | 67 | // then 68 | Optional optional = conversations.findBy("new conversation"); 69 | assertThat(optional.isPresent(), is(true)); 70 | Conversation conv = optional.get(); 71 | assertThat(conv.has(member), is(true)); 72 | assertThat(match.getMessage().getSignal(), is("created")); 73 | assertThat(match.getMessage().getCustom().get("type"), is("MESH")); 74 | assertThat(eventCall.getEvents().size(), is(1)); 75 | assertThat(eventCall.getEvents().get(0).type(), is(NextRTCEvents.CONVERSATION_CREATED)); 76 | } 77 | 78 | @Test 79 | public void shouldCreateConversation_BROADCAST() throws Exception { 80 | // given 81 | MessageMatcher match = new MessageMatcher(); 82 | Member member = mockMember("Jan", match); 83 | members.register(member); 84 | 85 | // when 86 | create.execute(InternalMessage.create()// 87 | .from(member)// 88 | .content("new conversation")// 89 | .addCustom("type", "BROADCAST") 90 | .build()); 91 | 92 | // then 93 | Optional optional = conversations.findBy("new conversation"); 94 | assertThat(optional.isPresent(), is(true)); 95 | Conversation conv = optional.get(); 96 | assertThat(conv.has(member), is(true)); 97 | assertThat(match.getMessage().getSignal(), is("created")); 98 | assertThat(match.getMessage().getCustom().get("type"), is("BROADCAST")); 99 | assertThat(eventCall.getEvents().size(), is(1)); 100 | assertThat(eventCall.getEvents().get(0).type(), is(NextRTCEvents.CONVERSATION_CREATED)); 101 | } 102 | 103 | @Test 104 | public void shouldThrowExceptionWhenConversationExists() throws Exception { 105 | // given 106 | Member other = mockMember("Other"); 107 | members.register(other); 108 | create.execute(InternalMessage.create()// 109 | .from(other)// 110 | .content("new conversation")// 111 | .build()); 112 | 113 | Member member = mockMember("Jan"); 114 | members.register(member); 115 | 116 | // then 117 | exception.expect(SignalingException.class); 118 | exception.expectMessage(CONVERSATION_NAME_OCCUPIED.name()); 119 | 120 | // when 121 | create.execute(InternalMessage.create()// 122 | .from(member)// 123 | .content("new conversation")// 124 | .build()); 125 | } 126 | 127 | @Test 128 | public void shouldThrowExceptionWhenUserIsInOtherConversation() throws Exception { 129 | // given 130 | MessageMatcher match = new MessageMatcher(); 131 | Member member = mockMember("Jan", match); 132 | members.register(member); 133 | create.execute(InternalMessage.create()// 134 | .from(member)// 135 | .content("new conversation")// 136 | .build()); 137 | 138 | // then 139 | exception.expect(SignalingException.class); 140 | exception.expectMessage(MEMBER_IN_OTHER_CONVERSATION.name()); 141 | 142 | // when 143 | create.execute(InternalMessage.create()// 144 | .from(member)// 145 | .content("second conversation")// 146 | .build()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/performance/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.performance; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.eclipse.jetty.websocket.client.WebSocketClient; 5 | import org.junit.Ignore; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.Parameterized; 9 | import org.junit.runners.Parameterized.Parameters; 10 | 11 | import java.net.URI; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Collection; 15 | import java.util.List; 16 | import java.util.stream.IntStream; 17 | 18 | import static java.lang.String.format; 19 | import static java.util.concurrent.TimeUnit.MINUTES; 20 | import static java.util.stream.Collectors.toList; 21 | import static org.awaitility.Awaitility.await; 22 | import static org.hamcrest.Matchers.*; 23 | import static org.junit.Assert.assertThat; 24 | 25 | 26 | @Ignore 27 | @Slf4j 28 | @RunWith(Parameterized.class) 29 | public class PerformanceTest { 30 | 31 | @Parameters(name = "{0}: on url {1}") 32 | public static Collection data() { 33 | return Arrays.asList(new Object[][]{ 34 | //{"RatPack", uri("localhost:5050")}, 35 | {"Spring", uri("localhost:8080")}//,{"Standalone", uri("localhost:8090")} 36 | }); 37 | } 38 | 39 | private URI uri; 40 | 41 | private static URI uri(String uri) { 42 | try { 43 | return new URI(format("wss://%s/signaling", uri)); 44 | } catch (Exception e) { 45 | throw new RuntimeException(e); 46 | } 47 | } 48 | 49 | public PerformanceTest(String type, URI uri) { 50 | this.uri = uri; 51 | } 52 | 53 | @Test 54 | public void scenario1_meshConversationWith64Participant() throws Exception { 55 | // given 56 | int size = 64; 57 | List> peers = IntStream.range(0, size) 58 | .mapToObj(Peer::new) 59 | .map(this::openSession) 60 | .collect(toList()); 61 | 62 | // when 63 | peers.parallelStream() 64 | .map(Tuple::getSocket) 65 | .forEach(s -> s.join("x")); 66 | peers.stream() 67 | .map(Tuple::getSocket) 68 | .forEach(p -> await() 69 | .atMost(1, MINUTES) 70 | .until(() -> p.getJoinedTo() != null)); 71 | 72 | // then 73 | List result = peers.stream().map(Tuple::getSocket).collect(toList()); 74 | List allNames = result.stream().map(Peer::getName).collect(toList()); 75 | peers.stream() 76 | .map(Tuple::getSocket) 77 | .forEach(p -> await() 78 | .atMost(1, MINUTES) 79 | .until(() -> p.getJoined().size() == size - 1)); 80 | result.forEach(p -> assertThat(p.getName() + ": Some members were missed", p.getJoined(), containsInAnyOrder(without(allNames, p.getName())))); 81 | result.forEach(p -> assertThat(p.getName() + " doesn't have all participant!", p.getJoined(), hasSize(size - 1))); 82 | result.forEach(p -> assertThat(p.getName() + " has joined to himself!", p.getJoined(), not(containsInAnyOrder(p.getName())))); 83 | result.forEach(p -> await() 84 | .atMost(1, MINUTES) 85 | .until(() -> p.getCandidates().size() == size - 1)); 86 | result.forEach(p -> assertThat(p.getName() + " didn't exchange all candidates", p.getCandidates().size(), equalTo(size - 1))); 87 | result.forEach(p -> assertThat(" there are errors for " + p.getName() + ": " + p.getErrors(), p.getErrors(), hasSize(0))); 88 | peers.forEach(this::stop); 89 | } 90 | 91 | @Test 92 | public void scenario2_broadcastConversationWith64Participant() throws Exception { 93 | // given 94 | int size = 64; 95 | List> peers = IntStream.range(0, size + 1) 96 | .mapToObj(Peer::new) 97 | .map(this::openSession) 98 | .collect(toList()); 99 | 100 | // when 101 | Tuple masterTuple = peers.remove(0); 102 | Peer master = masterTuple.getSocket(); 103 | master.createConv("broadcast"); 104 | await() 105 | .atMost(1, MINUTES) 106 | .until(() -> master.getJoinedTo() != null); 107 | 108 | peers.parallelStream() 109 | .map(Tuple::getSocket) 110 | .forEach(s -> s.join("broadcast")); 111 | 112 | peers.stream() 113 | .map(Tuple::getSocket) 114 | .forEach(p -> await() 115 | .atMost(1, MINUTES) 116 | .until(() -> p.getJoinedTo() != null)); 117 | 118 | await() 119 | .atMost(1, MINUTES) 120 | .until(() -> master.getCandidates().size() == size); 121 | 122 | // then 123 | assertThat(master.getOfferRequests().size(), is(size)); 124 | assertThat(master.getFinalized().size(), is(size)); 125 | assertThat(master.getCandidates().size(), is(size)); 126 | List result = peers.stream().map(Tuple::getSocket).collect(toList()); 127 | List allNames = result.stream().map(Peer::getName).collect(toList()); 128 | assertThat(master.getCandidates().size(), is(size)); 129 | master.getCandidates().forEach((k, v) -> assertThat(allNames.contains(k), is(true))); 130 | result.forEach(p -> { 131 | assertThat(p.getJoinedTo(), is("broadcast")); 132 | assertThat(p.getJoined().size(), is(1)); 133 | assertThat(p.getCandidates().size(), is(1)); 134 | assertThat(p.getCandidates().keySet(), containsInAnyOrder(master.getName())); 135 | }); 136 | 137 | peers.forEach(this::stop); 138 | stop(masterTuple); 139 | } 140 | 141 | private String[] without(List allNames, String name) { 142 | List mod = new ArrayList<>(allNames); 143 | mod.remove(name); 144 | return mod.toArray(new String[0]); 145 | } 146 | 147 | private void stop(Tuple p) { 148 | try { 149 | p.getClient().stop(); 150 | } catch (Exception e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | 155 | private Tuple openSession(T socket) { 156 | WebSocketClient client = new WebSocketClient(); 157 | try { 158 | client.start(); 159 | client.connect(socket, uri); 160 | } catch (Exception e) { 161 | throw new RuntimeException(e); 162 | } 163 | return new Tuple<>(client, socket); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/test/java/org/nextrtc/signalingserver/performance/Peer.java: -------------------------------------------------------------------------------- 1 | package org.nextrtc.signalingserver.performance; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import lombok.Getter; 6 | import org.eclipse.jetty.websocket.api.Session; 7 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; 8 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 9 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 10 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 11 | import org.nextrtc.signalingserver.domain.Message; 12 | 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.ExecutorService; 19 | import java.util.concurrent.Executors; 20 | 21 | import static java.lang.String.format; 22 | import static java.util.Collections.synchronizedList; 23 | import static org.awaitility.Awaitility.await; 24 | import static org.nextrtc.signalingserver.domain.Message.create; 25 | 26 | @WebSocket 27 | @Getter 28 | public class Peer { 29 | private final static ExecutorService service = Executors.newCachedThreadPool(); 30 | private final Gson gson = new GsonBuilder().create(); 31 | private Session session; 32 | private String name; 33 | private String joinedTo; 34 | private List offerRequests = synchronizedList(new ArrayList<>()); 35 | private List answerRequests = synchronizedList(new ArrayList<>()); 36 | private List finalized = synchronizedList(new ArrayList<>()); 37 | private Map actions = new ConcurrentHashMap<>(); 38 | private Map> candidates = new ConcurrentHashMap<>(); 39 | private List joined = synchronizedList(new ArrayList<>()); 40 | private List errors = synchronizedList(new ArrayList<>()); 41 | private List log = synchronizedList(new ArrayList<>()); 42 | 43 | public Peer(int i) { 44 | actions.put("created", (s, msg) -> { 45 | name = msg.getTo(); 46 | joinedTo = msg.getContent(); 47 | }); 48 | actions.put("joined", (s, msg) -> { 49 | name = msg.getTo(); 50 | joinedTo = msg.getContent(); 51 | }); 52 | actions.put("offerrequest", (s, msg) -> { 53 | offerRequests.add(msg); 54 | send(create() 55 | .to(msg.getFrom()) 56 | .signal("offerResponse") 57 | .content("offer from" + name) 58 | .build()); 59 | }); 60 | actions.put("answerrequest", (s, msg) -> { 61 | answerRequests.add(msg); 62 | send(create() 63 | .to(msg.getFrom()) 64 | .signal("answerResponse") 65 | .content("answer from" + name) 66 | .build()); 67 | }); 68 | actions.put("finalize", (s, msg) -> { 69 | finalized.add(msg); 70 | send(create() 71 | .to(msg.getFrom()) 72 | .signal("candidate") 73 | .content("local candidate from " + name) 74 | .build()); 75 | send(create() 76 | .to(msg.getFrom()) 77 | .signal("candidate") 78 | .content("remote candidate from " + name) 79 | .build()); 80 | }); 81 | actions.put("candidate", (s, msg) -> { 82 | candidates.computeIfAbsent(msg.getFrom(), k -> new ArrayList<>()); 83 | candidates.get(msg.getFrom()).add(msg.getContent()); 84 | if (!msg.getContent().contains("answer")) { 85 | send(create() 86 | .to(msg.getFrom()) 87 | .signal("candidate") 88 | .content("answer from " + name + " on " + msg.getContent()) 89 | .build()); 90 | } 91 | }); 92 | actions.put("ping", (s, m) -> { 93 | }); 94 | actions.put("newjoined", (s, msg) -> { 95 | joined.add(msg.getContent()); 96 | }); 97 | actions.put("error", (s, msg) -> { 98 | errors.add(msg.getContent()); 99 | }); 100 | actions.put("end", (s, msg) -> { 101 | throw new RuntimeException(msg.getContent()); 102 | }); 103 | } 104 | 105 | public void join(String name) { 106 | send(create() 107 | .signal("join") 108 | .content(name) 109 | .build()); 110 | } 111 | 112 | public void createConv(String name) { 113 | Map custom = new HashMap<>(); 114 | custom.put("type", "BROADCAST"); 115 | send(create() 116 | .signal("create") 117 | .content(name) 118 | .custom(custom) 119 | .build()); 120 | } 121 | 122 | public void leave() { 123 | send(create() 124 | .signal("left") 125 | .build()); 126 | } 127 | 128 | @OnWebSocketConnect 129 | public void onConnect(Session session) { 130 | this.session = session; 131 | } 132 | 133 | @OnWebSocketMessage 134 | public void onMessage(String msg) { 135 | Message message = gson.fromJson(msg, Message.class); 136 | if (!"ping".equalsIgnoreCase(message.getSignal())) { 137 | log.add(message); 138 | } 139 | service.submit(() -> { 140 | String key = message.getSignal().toLowerCase(); 141 | if (actions.containsKey(key)) { 142 | actions.get(key).execute(session, message); 143 | } 144 | }); 145 | } 146 | 147 | @OnWebSocketClose 148 | public void onClose(int status, String reason) { 149 | session = null; 150 | log.add(create().content(reason).build()); 151 | } 152 | 153 | private void send(Message message) { 154 | log.add(message); 155 | service.submit(() -> { 156 | await().until(() -> getSession() != null); 157 | try { 158 | session.getRemote().sendStringByFuture(gson.toJson(message)); 159 | } catch (Exception e) { 160 | throw new RuntimeException(e); 161 | } 162 | }); 163 | } 164 | 165 | private interface Action { 166 | void execute(Session session, Message message); 167 | } 168 | 169 | @Override 170 | public String toString() { 171 | return format("%s joinedTo %s, received information about joining from %s participant. " + 172 | "Received candidates from %s persons. Has %s errors", 173 | name, getJoinedTo(), getJoined().size(), getCandidates().size(), errors.size()); 174 | } 175 | } 176 | --------------------------------------------------------------------------------