Everything you need is at http://nextrtc.org/ ;)
NextRTC is a rich java library providing WebRTC signaling server. You can use it as standalone web application, or add it as a tenant to your existing Spring application. It uses WebSocket (JSR 356) to communicate with clients. Front end client to NextRTC is available here.
NextRTC project use Lombok, so please be aware that you have to install Lombok plugin to you IDE.
NextRTC can be used in two modes Standalone or as a Spring module. Details about frontend client can be found here. Both mode setup is described below.
There are 5 project that can interest you:
- Web chat using Spring (Spring WebSocket)
- Web chat using Tomcat (via Cargo plugin) without Spring but with websocket JSR-356
- Web chat using RatPack without Spring with Netty WebSocket implementation
- Web chat with Spring + SpringWebSocket + Spring Security
- NextRTC JS Client
- Sample js application that is used in the all examples
If you want to use NextRTC as a module of existing Spring based solution you have to add to you pom.xml file following entry:
<dependencies>
<dependency>
<groupId>org.nextrtc.signalingserver</groupId>
<artifactId>nextrtc-signaling-server</artifactId>
<version>${current-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
</dependencies>
Latest version of NextRTC can be found here.
Then you have to create a config where your endpoint will be defined
@Configuration
@Import(NextRTCConfig.class)
public class MyWebSocketConfigurator implements WebSocketConfigurer {
@Autowired
private NextRTCServer server;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(customEndpoint(), "/signaling").setAllowedOrigins("*");
}
@Bean
public MyEndpoint customEndpoint() {
return new MyEndpoint(server);
}
}
And implement Spring WebSocket handler
public class MyEndpoint extends TextWebSocketHandler {
private static class SessionWrapper implements Connection {
private final WebSocketSession session;
public SessionWrapper(WebSocketSession session) {
this.session = session;
}
@Override
public String getId() {
return session.getId();
}
@Override
public boolean isOpen() {
return session.isOpen();
}
@Override
public void sendObject(Object object) {
try {
session.sendMessage(new TextMessage(NextRTCServer.MessageEncoder.encode(object)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private final NextRTCServer server;
@Inject
MyEndpoint(NextRTCServer server) {
this.server = server;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
server.register(new SessionWrapper(session));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
server.handle(NextRTCServer.MessageDecoder.decode(message.getPayload()), new SessionWrapper(session));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
server.unregister(new SessionWrapper(session), status.getReason());
}
}
That is all what you need to make NextRTC up and running (from the back-end point of view). You can find working example here.
In configuration class what you have to do is autowire SignalResolver and MessageSender (this one is required to send message to member).
@Configuration
class Config {
@Autowired
private SignalResolver resolver;
@Autowired
private MessageSender sender;
}
Then in the same Config file you have to add bean which will add your handler to signal resolver. Handler has to implement interface SignalHandler
. This interface has only one method with one parameter. This parameter has type InternalMessage
. In parameter of this method you will always have from
field with member that sent message. If your client provides in request destination member, field to
will be filled with appropriate member.
That interface has only one method, so I this example I'll inline it to lambda expression:
@Configuration
class Config {
@Bean
public Signal addCustomNextRTCHandlers(){
Signal upperCase = Signal.fromString("upperCase");
resolver.addCustomHandler(upperCase, (msg)->
sender.send(InternalMessage.create()
.to(msg.getFrom())
.content(msg.getContent().toUpperCase())
.signal(upperCase)
.build())
);
return upperCase;
}
}
In this example new handler (upperCase) will take a content of incoming message and resend the content to sender with uppercase letters.
If you want to handle event that comes from server you have to create class that implements interface implements NextRTCHandler
, and give annotation @NextRTCEventListener(<event>)
. Class that implements interface and has EventListener annotation has to be registered in Spring container. Here is example with basic handler.
@Component
@NextRTCEventListener(UNEXPECTED_SITUATION)
public class ExceptionHandler implements NextRTCHandler {
private static final Logger log = Logger.getLogger(ExceptionHandler.class);
@Override
public void handleEvent(NextRTCEvent nextRTCEvent) {
log.error(nextRTCEvent);
}
}
If you want to use NextRTC in standalone mode you have to add it as a Maven dependency
<dependencies>
<dependency>
<groupId>org.nextrtc.signalingserver</groupId>
<artifactId>nextrtc-signaling-server</artifactId>
<version>${current-version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
Latest version of NextRTC can be found here. Then you have to create NextRTCServer instance. And use it inside your WebSocket handler. Below there are examples of usage that can be found here (ratpack) and here (standalone tomcat).
// RatPack implementation
public class RatPackWebSocketHandler implements WebSocketHandler<String> {
private final NextRTCServer server;
private RatPackConnection connection;
private static class RatPackConnection implements Connection {
private static final AtomicLong nextId = new AtomicLong(1);
private final WebSocket socket;
private String id;
private RatPackConnection(WebSocket socket) {
this.id = "0xx" + nextId.getAndIncrement();
this.socket = socket;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isOpen() {
return socket.isOpen();
}
@Override
public void sendObject(Object object) {
socket.send(NextRTCServer.MessageEncoder.encode(object));
}
}
public RatPackWebSocketHandler(NextRTCServer server) {
this.server = server;
}
@Override
public String onOpen(WebSocket webSocket) throws Exception {
connection = new RatPackConnection(webSocket);
server.register(connection);
return null;
}
@Override
public void onClose(WebSocketClose<String> webSocketClose) throws Exception {
server.unregister(connection, webSocketClose.getOpenResult());
}
@Override
public void onMessage(WebSocketMessage<String> webSocketMessage) throws Exception {
server.handle(webSocketMessage.getText(), connection);
}
}
When you're using tomcat as a servlet container you'll need to add two dependencies (websocket-api and servlet-api)
<dependencies>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.nextrtc.signalingserver</groupId>
<artifactId>nextrtc-signaling-server</artifactId>
<version>${current-version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
In standalone mode you probably have to add to your project directory webapp/WEB-INF/web.xml
with content is similar to this provided below:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
</web-app>
And implement @ServerEndpoint as it was presented below
@ServerEndpoint(value = "/signaling")
public class MyEndpoint {
private static final Logger log = LoggerFactory.getLogger(MyEndpoint.class);
private static final NextRTCServer server = NextRTCServer.create(configuration -> {
configuration.nextRTCProperties().setPingPeriod(1);
configuration.signalResolver().addCustomSignal(Signal.fromString("upperCase"), (msg) ->
configuration.messageSender().send(InternalMessage.create()
.to(msg.getFrom())
.signal(Signal.fromString("upperCase"))
.content(msg.getContent() == null ? "" : msg.getContent().toUpperCase())
.build()));
configuration.eventDispatcher().addListener(new CustomHandler());
return configuration;
});
private static class SessionWrapper implements Connection {
private final Session session;
public SessionWrapper(Session session) {
this.session = session;
}
@Override
public String getId() {
return session.getId();
}
@Override
public boolean isOpen() {
return session.isOpen();
}
@Override
public void sendObject(Object object) {
try {
session.getBasicRemote().sendText(NextRTCServer.MessageEncoder.encode(object));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
server.register(new SessionWrapper(session));
}
@OnMessage
public void onMessage(String message, Session session) {
server.handle(NextRTCServer.MessageDecoder.decode(message), new SessionWrapper(session));
}
@OnClose
public void onClose(Session session, CloseReason reason) {
server.unregister(new SessionWrapper(session), reason.getReasonPhrase());
}
@OnError
public void onError(Session session, Throwable exception) {
server.handleError(new SessionWrapper(session), exception);
}
}
Without web.xml
servlet container sometimes doesn't scan classes and your Endpoint can be omitted during class loading.
You can find working example here.
In manualConfiguration method if you want to provide your own signal you have to register it during server creation.
NextRTCServer server = NextRTCServer.create(configuration -> {
configuration.signalResolver().addCustomSignal(/* custom signal is going here */);
return configuration;
});
Configuration object provides you ability to modify properties configuration.nextRTCProperties()
, and it also provides you configuration.signalResolver()
. To add your own signal you have to just add to signal resolver your own signal
private static final NextRTCServer server = NextRTCServer.create(configuration -> {
configuration.nextRTCProperties().setPingPeriod(1);
configuration.signalResolver().addCustomSignal(Signal.fromString("upperCase"), (msg) ->
configuration.messageSender().send(InternalMessage.create()
.to(msg.getFrom())
.signal(Signal.fromString("upperCase"))
.content(msg.getContent() == null ? "" : msg.getContent().toUpperCase())
.build()));
configuration.eventDispatcher().addListener(new CustomHandler());
return configuration;
});
}
In this example new handler (upperCase) will take a content of incoming message and resend the content to sender with uppercase letters.
If you want to handle event that comes from server you have to create class that implements interface implements NextRTCHandler
, and give annotation @NextRTCEventListener(<event>)
. Class that implements interface and has EventListener annotation has to be registered during manual configuration. Here is example with basic handler.
@NextRTCEventListener(UNEXPECTED_SITUATION)
public class ExceptionHandler implements NextRTCHandler {
private static final Logger log = Logger.getLogger(ExceptionHandler.class);
@Override
public void handleEvent(NextRTCEvent nextRTCEvent) {
log.error(nextRTCEvent);
}
}
...
private static final NextRTCServer server = NextRTCServer.create(configuration -> {
configuration.eventDispatcher().addListener(new ExceptionHandler());
return configuration;
});
}
When you will write your own signal, you probably will send a result of processing to some member of conversation. To do so you just have to do two things.
Firstly you have to find member to whom you want to send message. You can do it by autowiring MemberRepository
. Bean that implement this interface will return member to whom you can easily send message.
class SomeClass {
@Autowired
private MemberRepository members;
public void fetchById(String id){
Optional<Member> memberOptional = members.findBy(id);
}
}
To be able to send a message to member you have to autowire MessageSender
, then create an InternalMessage
and send it.
class SendMessage {
@Autowired
private MessageSender sender;
public void sendTo(Member to){
sender.send(InternalMessage.create()
.to(to)
.content("whatever you like")
.signal(Signal.fromString("my_signal"))
.build()); // method send will send message synchronously
}
}
In standalone mode you have to do the same. You can find MessageSender in EndpointConfiguration bean.
NextRTC uses event bus to communicate with other components. Internally NextRTC is taking request object then based on signal field is looking for appropriate handler. When handler exists it code is executed. During execution NextRTC can send messages to client via websocket and can produce predefined events on what you can react in your application.
You can react on following events:
- SESSION_OPENED
- Is posted when client opens websocket channel
- SESSION_CLOSED
- Is posted when client close websocket channel
- CONVERSATION_CREATED
- Is posted when conversation has been created
- CONVERSATION_DESTROYED
- Is posted when conversation has been destroyed
- UNEXPECTED_SITUATION
- Is posted on every unexpected situation - broken pipe, forbidden action
- MEMBER_JOINED
- Is posted when member has been added to conversation
- MEMBER_LEFT
- Is posted when member has left the conversation
- MEDIA_LOCAL_STREAM_REQUESTED
- Is posted after offerRequest / answerRequest signals are sent to clients
- MEDIA_LOCAL_STREAM_CREATED
- Is posted when signal offerResponse / answerResponse arrives to server
- MEDIA_STREAMING
- Is posted when clients exchanged all required signals to connect themselves by WebRTC
- TEXT
- Is posted on each text message coming from client
To be able to react on event you have to write java class which implements NextRTCHandler
and has annotation @NextRTCEventListener
. You can customize on what certain event your handler will react by giving it name to annotation @NextRTCEventListener(UNEXPECTED_SITUATION)
. If you are using NextRTC with Spring then your handler should be also annotated with one of Spring stereotype annotations (@Service, @Component ...)
Each request coming in and out of server have structure like this:
{
"from": "",
"to": "",
"signal": "",
"content": "",
"custom" : {}
}
custom can contains only string properties, deeper nesting will produce error. Valid request/response can look like this:
{
"from": "John",
"to": "Alice",
"signal": "TEXT",
"content": "Hello Alice!",
"custom" : {
"myCustomHeader": "my custom value",
"otherHeader": "value"
}
}
Custom is used to determine version of conversation. When you are creating new conversation you can add to custom
field type
with value MESH
or BROADCAST
. Depending on value in type field signaling server will create different conversation type.
In signal field you can pass one of default defined signals:
- create
- join
- offerResponse
- answerResponse
- finalize
- candidate
- ping
- left
- text
Or give your own custom signal - how to define custom signals you can read here. Server can send to messages with default predefined signals or with custom defined by you. Default group consists of:
- created
- joined
- newJoined
- text
- offerRequest
- answerRequest
- candidate
- ping
- error
- end
If you want to check what has been done you can check CHANGELOG