diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index e6bcbb10..99c91774 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -76,7 +76,8 @@ export class Publisher extends StreamManager { this.properties = properties; this.openvidu = openvidu; - this.stream.ee.on('local-stream-destroyed-by-disconnect', (reason: string) => { + this.stream.ee.on('local-stream-destroyed', (reason: string) => { + this.stream.isLocalStreamPublished = false; const streamEvent = new StreamEvent(true, this, 'streamDestroyed', this.stream, reason); this.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 41b788ce..8914082f 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -330,7 +330,7 @@ export class Session implements EventDispatcher { publisher.session = this; publisher.stream.session = this; - if (!publisher.stream.isLocalStreamPublished) { + if (!publisher.stream.publishedOnce) { // 'Session.unpublish(Publisher)' has NOT been called this.connection.addStream(publisher.stream); publisher.stream.publish() @@ -413,6 +413,88 @@ export class Session implements EventDispatcher { } + /** + * Forces some user to leave the session + * + * #### Events dispatched + * + * The behavior is the same as when some user calls [[Session.disconnect]], but `reason` property in all events will be `"forceDisconnectByUser"`. + * + * The local [[Session]] object will dispatch: + * - A `streamDestroyed` event if the evicted user was publishing a stream, with property `reason` set to `"forceDisconnectByUser"` + * - A `connectionDestroyed` event for the evicted user, with property `reason` set to `"forceDisconnectByUser"` + * + * The remote [[Session]] object of every other participant will dispatch: + * - A `streamDestroyed` event if the evicted user was publishing a stream, with property `reason` set to `"forceDisconnectByUser"` + * - A `connectionDestroyed` event for the evicted user, with property `reason` set to `"forceDisconnectByUser"` + * + * If any, the [[Publisher]] object of the evicted participant will also dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"` + * + * @returns A Promise (to which you can optionally subscribe to) that is resolved only after the participant has been successfully evicted from the session and rejected with an Error object if not + */ + forceDisconnect(connection: Connection): Promise { + return new Promise((resolve, reject) => { + console.info('Forcing disconnect for connection ' + connection.connectionId); + this.openvidu.sendRequest( + 'forceDisconnect', + { connectionId: connection.connectionId }, + (error, response) => { + if (error) { + console.error('Error forcing disconnect for Connection ' + connection.connectionId, error); + if (error.code === 401) { + reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force a disconnection")); + } else { + reject(error); + } + } else { + console.info('Forcing disconnect correctly for Connection ' + connection.connectionId); + resolve(); + } + } + ); + }); + } + + + /** + * Forces some user to unpublish a Stream + * + * #### Events dispatched + * + * The behavior is the same as when some user calls [[Session.unpublish]], but `reason` property in all events will be `"forceUnpublishByUser"`. + * + * The local [[Session]] object will dispatch a `streamDestroyed` event with property `reason` set to `"forceUnpublishByUser"` + * + * The remote [[Session]] object of every other participant will dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"` + * + * The [[Publisher]] object of the affected participant will also dispatch a `streamDestroyed` event with property `reason` set to `"forceDisconnectByUser"` + * + * @returns A Promise (to which you can optionally subscribe to) that is resolved only after the remote Stream has been successfully unpublished from the session and rejected with an Error object if not + */ + forceUnpublish(stream: Stream): Promise { + return new Promise((resolve, reject) => { + console.info('Forcing unpublish for stream ' + stream.streamId); + this.openvidu.sendRequest( + 'forceUnpublish', + { streamId: stream.streamId }, + (error, response) => { + if (error) { + console.error('Error forcing unpublish for Stream ' + stream.streamId, error); + if (error.code === 401) { + reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force an unpublishing")); + } else { + reject(error); + } + } else { + console.info('Forcing unpublish correctly for Stream ' + stream.streamId); + resolve(); + } + } + ); + }); + } + + /** * Sends one signal. `signal` object has the following optional properties: * ```json @@ -630,23 +712,28 @@ export class Session implements EventDispatcher { * @hidden */ onParticipantUnpublished(msg): void { - this.getRemoteConnection(msg.connectionId, "Remote connection '" + msg.connectionId + "' unknown when 'onParticipantUnpublished'. " + - 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) + if (msg.connectionId === this.connection.connectionId) { + // Your stream has been forcedly unpublished from the session + this.stopPublisherStream(msg.reason); + } else { + this.getRemoteConnection(msg.connectionId, "Remote connection '" + msg.connectionId + "' unknown when 'onParticipantUnpublished'. " + + 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) - .then(connection => { + .then(connection => { - const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream, msg.reason); - this.ee.emitEvent('streamDestroyed', [streamEvent]); - streamEvent.callDefaultBehavior(); + const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream, msg.reason); + this.ee.emitEvent('streamDestroyed', [streamEvent]); + streamEvent.callDefaultBehavior(); - // Deleting the remote stream - const streamId: string = connection.stream.streamId; - delete this.remoteStreamsCreated[streamId]; - connection.removeStream(streamId); - }) - .catch(openViduError => { - console.error(openViduError); - }); + // Deleting the remote stream + const streamId: string = connection.stream.streamId; + delete this.remoteStreamsCreated[streamId]; + connection.removeStream(streamId); + }) + .catch(openViduError => { + console.error(openViduError); + }); + } } /** @@ -658,27 +745,6 @@ export class Session implements EventDispatcher { if (!!this.sessionId && !this.connection.disposed) { this.leave(true, msg.reason); } - } else { - // Other user has been evicted from the session - this.getRemoteConnection(msg.connectionId, 'Remote connection ' + msg.connectionId + " unknown when 'onParticipantEvicted'. " + - 'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections))) - - .then(connection => { - if (!!connection.stream) { - const stream = connection.stream; - - const streamEvent = new StreamEvent(true, this, 'streamDestroyed', stream, msg.reason); - this.ee.emitEvent('streamDestroyed', [streamEvent]); - streamEvent.callDefaultBehavior(); - - delete this.remoteStreamsCreated[stream.streamId]; - } - delete this.remoteConnections[connection.connectionId]; - this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, msg.reason)]); - }) - .catch(openViduError => { - console.error(openViduError); - }); } } @@ -860,14 +926,7 @@ export class Session implements EventDispatcher { this.openvidu.closeWs(); } - if (!!this.connection.stream) { - // Dispose Publisher's local stream - this.connection.stream.disposeWebRtcPeer(); - if (this.connection.stream.isLocalStreamPublished) { - // Make Publisher object dispatch 'streamDestroyed' event if the Stream was published - this.connection.stream.ee.emitEvent('local-stream-destroyed-by-disconnect', [reason]); - } - } + this.stopPublisherStream(reason); if (!this.connection.disposed) { // Make Session object dispatch 'sessionDisconnected' event (if it is not already disposed) @@ -906,7 +965,9 @@ export class Session implements EventDispatcher { // Initialize capabilities object with the role this.capabilities = { subscribe: true, - publish: this.openvidu.role !== 'SUBSCRIBER' + publish: this.openvidu.role !== 'SUBSCRIBER', + forceUnpublish: this.openvidu.role === 'MODERATOR', + forceDisconnect: this.openvidu.role === 'MODERATOR' }; // Initialize local Connection object with values returned by openvidu-server @@ -951,6 +1012,17 @@ export class Session implements EventDispatcher { }); } + private stopPublisherStream(reason: string) { + if (!!this.connection.stream) { + // Dispose Publisher's local stream + this.connection.stream.disposeWebRtcPeer(); + if (this.connection.stream.isLocalStreamPublished) { + // Make Publisher object dispatch 'streamDestroyed' event if the Stream was published + this.connection.stream.ee.emitEvent('local-stream-destroyed', [reason]); + } + } + } + private stringClientMetadata(metadata: any): string { if (typeof metadata !== 'string') { return JSON.stringify(metadata); diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 58cf4bf3..1a46bf85 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -26,6 +26,7 @@ import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpea import EventEmitter = require('wolfy87-eventemitter'); import hark = require('hark'); +import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; /** @@ -118,6 +119,10 @@ export class Stream { * @hidden */ isLocalStreamPublished = false; + /** + * @hidden + */ + publishedOnce = false; /** * @hidden */ @@ -465,12 +470,17 @@ export class Stream { videoDimensions: JSON.stringify(this.videoDimensions) }, (error, response) => { if (error) { - reject('Error on publishVideo: ' + JSON.stringify(error)); + if (error.code === 401) { + reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish")); + } else { + reject('Error on publishVideo: ' + JSON.stringify(error)); + } } else { this.webRtcPeer.processAnswer(response.sdpAnswer) .then(() => { this.streamId = response.id; this.isLocalStreamPublished = true; + this.publishedOnce = true; if (this.displayMyRemote()) { this.remotePeerSuccessfullyEstablished(); } diff --git a/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts b/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts index 26f5a1e5..5c64469b 100644 --- a/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts +++ b/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts @@ -82,7 +82,8 @@ export enum OpenViduErrorName { PUBLISHER_PROPERTIES_ERROR = 'PUBLISHER_PROPERTIES_ERROR', /** - * _Not in use yet_ + * The client tried to call a method without the required permissions. This can occur for methods [[Session.publish]], + * [[Session.forceUnpublish]] and [[Session.forceDisconnect]] */ OPENVIDU_PERMISSION_DENIED = 'OPENVIDU_PERMISSION_DENIED', diff --git a/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts index bc0040c6..28d8c868 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts @@ -35,6 +35,8 @@ export class ConnectionEvent extends Event { /** * For 'connectionDestroyed' event: * - "disconnect": the remote user has called `Session.disconnect()` + * - "forceDisconnectByUser": the remote user has been evicted from the Session by other user calling `Session.forceDisconnect()` + * - "forceDisconnectByServer": the remote user has been evicted from the Session by the application * - "networkDisconnect": the remote user network connection has dropped * * For 'connectionCreated' empty string diff --git a/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts index 6b1ff6d8..1d88bb11 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts @@ -34,8 +34,8 @@ export class RecordingEvent extends Event { /** * The recording name you supplied to openvidu-server. For example, to name your recording file MY_RECORDING: * - With **API REST**: POST to `/api/recordings/start` passing JSON body `{"session":"sessionId","name":"MY_RECORDING"}` - * - With **openvidu-java-client**: `OpenVidu.startRecording(sessionId, MY_RECORDING)` or `OpenVidu.startRecording(sessionId, new RecordingProperties.Builder().name(MY_RECORDING).build())` - * - With **openvidu-node-client**: `OpenVidu.startRecording(sessionId, MY_RECORDING)` or `OpenVidu.startRecording(sessionId, new RecordingProperties.Builder().name(MY_RECORDING).build())` + * - With **openvidu-java-client**: `OpenVidu.startRecording(sessionId, "MY_RECORDING")` or `OpenVidu.startRecording(sessionId, new RecordingProperties.Builder().name("MY_RECORDING").build())` + * - With **openvidu-node-client**: `OpenVidu.startRecording(sessionId, "MY_RECORDING")` or `OpenVidu.startRecording(sessionId, {name: "MY_RECORDING"})` * * If no name is supplied, this property will be undefined and the recorded file will be named after property [[id]] */ diff --git a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts index 49933af6..0887f7ca 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts @@ -26,6 +26,9 @@ export class SessionDisconnectedEvent extends Event { /** * - "disconnect": you have called `Session.disconnect()` + * - "forceDisconnectByUser": you have been evicted from the Session by other user calling `Session.forceDisconnect()` + * - "forceDisconnectByServer": you have been evicted from the Session by the application + * - "sessionClosedByServer": the Session has been closed by the application * - "networkDisconnect": your network connection has dropped */ reason: string; diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts index 90b6186b..a0408aa6 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts @@ -37,6 +37,11 @@ export class StreamEvent extends Event { * For 'streamDestroyed' event: * - "unpublish": method `Session.unpublish()` has been called * - "disconnect": method `Session.disconnect()` has been called + * - "forceUnpublishByUser": some user has called `Session.forceUnpublish()` over the Stream + * - "forceDisconnectByUser": some user has called `Session.forceDisconnect()` over the Stream + * - "forceUnpublishByServer": the user's stream has been unpublished from the Session by the application + * - "forceDisconnectByServer": the user has been evicted from the Session by the application + * - "sessionClosedByServer": the Session has been closed by the application * - "networkDisconnect": the user's network connection has dropped * * For 'streamCreated' empty string diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts index e55c805c..1112735e 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts @@ -20,6 +20,16 @@ */ export interface Capabilities { + /** + * `true` if the client can call [[Session.forceDisconnect]], `false` if not + */ + forceDisconnect: boolean; + + /** + * `true` if the client can call [[Session.forceUnpublish]], `false` if not + */ + forceUnpublish: boolean; + /** * `true` if the client can call [[Session.publish]], `false` if not */ diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts index adbf93e8..3e936471 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts @@ -35,22 +35,22 @@ export interface StreamManagerVideo { /** * The DOM HTMLElement assigned as target element when creating a video for the StreamManager. This property is defined when: * - [[OpenVidu.initPublisher]] or [[Session.subscribe]] methods have been called passing a valid `targetElement` parameter. - * - [[SessionManager.createVideoElement]] has been called. + * - [[StreamManager.createVideoElement]] has been called. * * This property is undefined when: * - [[OpenVidu.initPublisher]] or [[Session.subscribe]] methods have been called passing *null* or *undefined* as `targetElement` parameter. - * - [[SessionManager.addVideoElement]] has been called. + * - [[StreamManager.addVideoElement]] has been called. */ targetElement?: HTMLElement; /** * How the DOM video element should be inserted with respect to `targetElement`. This property is defined when: * - [[OpenVidu.initPublisher]] or [[Session.subscribe]] methods have been called passing a valid `targetElement` parameter. - * - [[SessionManager.createVideoElement]] has been called. + * - [[StreamManager.createVideoElement]] has been called. * * This property is undefined when: * - [[OpenVidu.initPublisher]] or [[Session.subscribe]] methods have been called passing *null* or *undefined* as `targetElement` parameter. - * - [[SessionManager.addVideoElement]] has been called. + * - [[StreamManager.addVideoElement]] has been called. */ insertMode?: VideoInsertMode; diff --git a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java index fffed01d..f2cc5846 100644 --- a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java +++ b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java @@ -87,7 +87,13 @@ public class ProtocolElements { public static final String STREAMPROPERTYCHANGED_PROPERTY_PARAM = "property"; public static final String STREAMPROPERTYCHANGED_NEWVALUE_PARAM = "newValue"; public static final String STREAMPROPERTYCHANGED_REASON_PARAM = "reason"; - + + public static final String FORCEDISCONNECT_METHOD = "forceDisconnect"; + public static final String FORCEDISCONNECT_CONNECTIONID_PARAM = "connectionId"; + + public static final String FORCEUNPUBLISH_METHOD = "forceUnpublish"; + public static final String FORCEUNPUBLISH_STREAMID_PARAM = "streamId"; + // ---------------------------- SERVER RESPONSES & EVENTS ----------------- public static final String PARTICIPANTJOINED_METHOD = "participantJoined"; diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenViduRole.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenViduRole.java index 574ebb68..c47bb5fd 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenViduRole.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenViduRole.java @@ -20,19 +20,18 @@ package io.openvidu.java.client; public enum OpenViduRole { /** - * Can subscribe to published streams of other users + * Can subscribe to published Streams of other users */ SUBSCRIBER, /** - * SUBSCRIBER permissions + can publish their own streams + * SUBSCRIBER permissions + can publish their own Streams (call Session.publish()) */ PUBLISHER, /** - * (not available yet) SUBSCRIBER and PUBLIHSER permissions + can force - * unpublish() and disconnect() over a third-party - * stream or user + * SUBSCRIBER + PUBLISHER permissions + can force the unpublishing or disconnection over a third-party Stream or Connection + * (call Session.forceUnpublish() and Session.forceDisconnect()) */ MODERATOR; } diff --git a/openvidu-node-client/src/OpenViduRole.ts b/openvidu-node-client/src/OpenViduRole.ts index 2ebdf49b..4c0e2091 100644 --- a/openvidu-node-client/src/OpenViduRole.ts +++ b/openvidu-node-client/src/OpenViduRole.ts @@ -18,17 +18,18 @@ export enum OpenViduRole { /** - * Can subscribe to published streams of other users + * Can subscribe to published Streams of other users */ SUBSCRIBER = 'SUBSCRIBER', /** - * SUBSCRIBER permissions + can publish their own streams + * SUBSCRIBER permissions + can publish their own Streams (call `Session.publish()`) */ PUBLISHER = 'PUBLISHER', /** - * _(not available yet)_ SUBSCRIBER + PUBLIHSER permissions + can force `unpublish()` and `disconnect()` over a third-party stream or user + * SUBSCRIBER + PUBLISHER permissions + can force the unpublishing or disconnection over a third-party Stream or Connection + * (call `Session.forceUnpublish()` and `Session.forceDisconnect()`) */ MODERATOR = 'MODERATOR' } \ No newline at end of file diff --git a/openvidu-node-client/src/TokenOptions.ts b/openvidu-node-client/src/TokenOptions.ts index 66be778d..d84a7fd9 100644 --- a/openvidu-node-client/src/TokenOptions.ts +++ b/openvidu-node-client/src/TokenOptions.ts @@ -25,10 +25,10 @@ export interface TokenOptions { * - If you have provided some data when calling `Session.connect(TOKEN, DATA)` (`DATA` defined), then `Connection.data` will have the following structure: `"CLIENT_DATA%/%SERVER_DATA"`, being `CLIENT_DATA` the second * parameter passed in OpenVidu Browser in method `Session.connect` and `SERVER_DATA` this [[TokenOptions.data]] property. */ - data: string; + data?: string; /** * The role assigned to this token */ - role: OpenViduRole; + role?: OpenViduRole; } \ No newline at end of file diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index 70fac139..ac00111b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -48,6 +48,7 @@ import com.google.gson.JsonParser; import io.openvidu.server.cdr.CDRLoggerFile; import io.openvidu.server.cdr.CallDetailRecord; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionManager; import io.openvidu.server.coturn.CoturnCredentialsService; import io.openvidu.server.coturn.CoturnCredentialsServiceFactory; @@ -126,7 +127,7 @@ public class OpenViduServer implements JsonRpcConfigurer { @Bean @ConditionalOnMissingBean - public KurentoSessionEventsHandler kurentoSessionEventsHandler() { + public SessionEventsHandler sessionEventsHandler() { return new KurentoSessionEventsHandler(); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java index 1876131f..80abae51 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java @@ -44,19 +44,28 @@ public class OpenviduConfig { private String openviduRecordingPath; @Value("${openvidu.recording.public-access}") - boolean openviduRecordingPublicAccess; + private boolean openviduRecordingPublicAccess; @Value("${openvidu.recording.notification}") - String openviduRecordingNotification; + private String openviduRecordingNotification; @Value("${openvidu.recording.custom-layout}") - String openviduRecordingCustomLayout; + private String openviduRecordingCustomLayout; @Value("${openvidu.recording.version}") - String openviduRecordingVersion; + private String openviduRecordingVersion; - @Value("#{'${spring.profiles.active:}'.length() > 0 ? '${spring.profiles.active:}'.split(',') : \"default\"}") - private String springProfile; + @Value("${openvidu.streams.video.max-recv-bandwidth}") + private int openviduStreamsVideoMaxRecvBandwidth; + + @Value("${openvidu.streams.video.min-recv-bandwidth}") + private int openviduStreamsVideoMinRecvBandwidth; + + @Value("${openvidu.streams.video.max-send-bandwidth}") + private int openviduStreamsVideoMaxSendBandwidth; + + @Value("${openvidu.streams.video.min-send-bandwidth}") + private int openviduStreamsVideoMinSendBandwidth; @Value("${coturn.redis.ip}") private String coturnRedisIp; @@ -70,6 +79,9 @@ public class OpenviduConfig { @Value("${coturn.redis.connect-timeout}") private String coturnRedisConnectTimeout; + @Value("#{'${spring.profiles.active:}'.length() > 0 ? '${spring.profiles.active:}'.split(',') : \"default\"}") + private String springProfile; + private String finalUrl; public String getOpenViduPublicUrl() { @@ -132,11 +144,27 @@ public class OpenviduConfig { return springProfile; } + public int getVideoMaxRecvBandwidth() { + return this.openviduStreamsVideoMaxRecvBandwidth; + } + + public int getVideoMinRecvBandwidth() { + return this.openviduStreamsVideoMinRecvBandwidth; + } + + public int getVideoMaxSendBandwidth() { + return this.openviduStreamsVideoMaxSendBandwidth; + } + + public int getVideoMinSendBandwidth() { + return this.openviduStreamsVideoMinSendBandwidth; + } + public String getCoturnDatabaseString() { return "\"ip=" + this.coturnRedisIp + " dbname=" + this.coturnRedisDbname + " password=" + this.coturnRedisPassword + " connect_timeout=" + this.coturnRedisConnectTimeout + "\""; } - + public String getCoturnDatabaseDbname() { return this.coturnRedisDbname; } @@ -147,6 +175,9 @@ public class OpenviduConfig { case "none": roles = new ParticipantRole[0]; break; + case "moderator": + roles = new ParticipantRole[] { ParticipantRole.MODERATOR }; + break; case "publisher_moderator": roles = new ParticipantRole[] { ParticipantRole.PUBLISHER, ParticipantRole.MODERATOR }; break; diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java index c119e42e..6fc89fcd 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java @@ -93,7 +93,7 @@ public class Participant { this.streaming = streaming; } - public String getPublisherStremId() { + public String getPublisherStreamId() { return null; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java index d9a80e30..a5f5d346 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java @@ -26,13 +26,13 @@ import io.openvidu.java.client.SessionProperties; public interface Session { String getSessionId(); - + SessionProperties getSessionProperties(); void join(Participant participant); - + void leave(String participantPrivateId, String reason); - + boolean close(String reason); boolean isClosed(); @@ -44,9 +44,9 @@ public interface Session { Participant getParticipantByPublicId(String participantPublicId); int getActivePublishers(); - + JSONObject toJSON(); - + JSONObject withStatsToJSON(); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java index 29e9d22a..a15d10ff 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java @@ -96,7 +96,7 @@ public class SessionEventsHandler { JsonObject stream = new JsonObject(); stream.addProperty(ProtocolElements.JOINROOM_PEERSTREAMID_PARAM, - existingParticipant.getPublisherStremId()); + existingParticipant.getPublisherStreamId()); stream.addProperty(ProtocolElements.JOINROOM_PEERSTREAMHASAUDIO_PARAM, kParticipant.getPublisherMediaOptions().hasAudio); stream.addProperty(ProtocolElements.JOINROOM_PEERSTREAMHASVIDEO_PARAM, @@ -214,13 +214,19 @@ public class SessionEventsHandler { } } - public void onUnpublishMedia(Participant participant, Set participants, Integer transactionId, - OpenViduException error, String reason) { - if (error != null) { - rpcNotificationService.sendErrorResponse(participant.getParticipantPrivateId(), transactionId, null, error); - return; + public void onUnpublishMedia(Participant participant, Set participants, Participant moderator, + Integer transactionId, OpenViduException error, String reason) { + boolean isRpcFromModerator = transactionId != null && moderator != null; + boolean isRpcFromOwner = transactionId != null && moderator == null; + + if (isRpcFromModerator) { + if (error != null) { + rpcNotificationService.sendErrorResponse(moderator.getParticipantPrivateId(), transactionId, null, + error); + return; + } + rpcNotificationService.sendResponse(moderator.getParticipantPrivateId(), transactionId, new JsonObject()); } - rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject()); JsonObject params = new JsonObject(); params.addProperty(ProtocolElements.PARTICIPANTUNPUBLISHED_NAME_PARAM, participant.getParticipantPublicId()); @@ -228,16 +234,28 @@ public class SessionEventsHandler { for (Participant p : participants) { if (p.getParticipantPrivateId().equals(participant.getParticipantPrivateId())) { - continue; + if (!isRpcFromOwner) { + rpcNotificationService.sendNotification(p.getParticipantPrivateId(), + ProtocolElements.PARTICIPANTUNPUBLISHED_METHOD, params); + } else { + if (error != null) { + rpcNotificationService.sendErrorResponse(p.getParticipantPrivateId(), transactionId, null, + error); + return; + } + rpcNotificationService.sendResponse(p.getParticipantPrivateId(), transactionId, new JsonObject()); + } } else { - rpcNotificationService.sendNotification(p.getParticipantPrivateId(), - ProtocolElements.PARTICIPANTUNPUBLISHED_METHOD, params); + if (error == null) { + rpcNotificationService.sendNotification(p.getParticipantPrivateId(), + ProtocolElements.PARTICIPANTUNPUBLISHED_METHOD, params); + } } } } - public void onSubscribe(Participant participant, Session session, String senderName, String sdpAnswer, - Integer transactionId, OpenViduException error) { + public void onSubscribe(Participant participant, Session session, String sdpAnswer, Integer transactionId, + OpenViduException error) { if (error != null) { rpcNotificationService.sendErrorResponse(participant.getParticipantPrivateId(), transactionId, null, error); return; @@ -260,8 +278,7 @@ public class SessionEventsHandler { } } - public void onUnsubscribe(Participant participant, String senderName, Integer transactionId, - OpenViduException error) { + public void onUnsubscribe(Participant participant, Integer transactionId, OpenViduException error) { if (error != null) { rpcNotificationService.sendErrorResponse(participant.getParticipantPrivateId(), transactionId, null, error); return; @@ -349,12 +366,30 @@ public class SessionEventsHandler { rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject()); } - public void onParticipantEvicted(Participant participant, String reason) { + public void onForceDisconnect(Participant moderator, Participant evictedParticipant, Set participants, + Integer transactionId, OpenViduException error, String reason) { + + boolean isRpcCall = transactionId != null; + if (isRpcCall) { + if (error != null) { + rpcNotificationService.sendErrorResponse(moderator.getParticipantPrivateId(), transactionId, null, + error); + return; + } + rpcNotificationService.sendResponse(moderator.getParticipantPrivateId(), transactionId, new JsonObject()); + } + JsonObject params = new JsonObject(); - params.addProperty(ProtocolElements.PARTICIPANTEVICTED_CONNECTIONID_PARAM, participant.getParticipantPublicId()); + params.addProperty(ProtocolElements.PARTICIPANTEVICTED_CONNECTIONID_PARAM, + evictedParticipant.getParticipantPublicId()); params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason); - rpcNotificationService.sendNotification(participant.getParticipantPrivateId(), + + rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(), ProtocolElements.PARTICIPANTEVICTED_METHOD, params); + for (Participant p : participants) { + rpcNotificationService.sendNotification(p.getParticipantPrivateId(), + ProtocolElements.PARTICIPANTEVICTED_METHOD, params); + } } public void sendRecordingStartedNotification(Session session, Recording recording) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index ab36a20a..224837f7 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -22,7 +22,6 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; import javax.annotation.PreDestroy; @@ -80,7 +79,7 @@ public abstract class SessionManager { public abstract void publishVideo(Participant participant, MediaOptions mediaOptions, Integer transactionId); - public abstract void unpublishVideo(Participant participant, Integer transactionId, String reason); + public abstract void unpublishVideo(Participant participant, Participant moderator, Integer transactionId, String reason); public abstract void subscribe(Participant participant, String senderName, String sdpOffer, Integer transactionId); @@ -94,15 +93,10 @@ public abstract class SessionManager { public abstract void onIceCandidate(Participant participant, String endpointName, String candidate, int sdpMLineIndex, String sdpMid, Integer transactionId); - /** - * Application-originated request to remove a participant from a session.
- * Side effects: The session event handler should notify the - * participant that she has been evicted. Should also send notifications to all - * other participants about the one that's just been evicted. - * - */ - public void evictParticipant(String participantPrivateId, String reason) throws OpenViduException { - } + public abstract boolean unpublishStream(Session session, String streamId, Participant moderator, Integer transactionId, String reason); + + public abstract void evictParticipant(Participant evictedParticipant, Participant moderator, Integer transactionId, + String reason); /** * Returns a Session given its id @@ -285,6 +279,18 @@ public abstract class SessionManager { } } + public boolean isModeratorInSession(String sessionId, Participant participant) { + if (!this.isInsecureParticipant(participant.getParticipantPrivateId())) { + if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) { + return ParticipantRole.MODERATOR.equals(participant.getToken().getRole()); + } else { + return false; + } + } else { + return true; + } + } + public boolean isInsecureParticipant(String participantPrivateId) { if (this.insecureUsers.containsKey(participantPrivateId)) { log.info("The user with private id {} is an INSECURE user", participantPrivateId); @@ -407,13 +413,11 @@ public abstract class SessionManager { throw new OpenViduException(Code.ROOM_CLOSED_ERROR_CODE, "Session '" + sessionId + "' already closed"); } Set participants = getParticipants(sessionId); - // copy the ids as they will be removed from the map - Set pids = participants.stream().map(Participant::getParticipantPrivateId).collect(Collectors.toSet()); - for (String pid : pids) { + for (Participant p : participants) { try { - this.evictParticipant(pid, reason); + this.evictParticipant(p, null, null, reason); } catch (OpenViduException e) { - log.warn("Error evicting participant with id '{}' from session '{}'", pid, sessionId, e); + log.warn("Error evicting participant '{}' from session '{}'", p.getParticipantPublicId(), sessionId, e); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/MutedMediaType.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/TrackType.java similarity index 95% rename from openvidu-server/src/main/java/io/openvidu/server/kurento/MutedMediaType.java rename to openvidu-server/src/main/java/io/openvidu/server/kurento/TrackType.java index 508c0cb8..f39648b2 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/MutedMediaType.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/TrackType.java @@ -17,6 +17,6 @@ package io.openvidu.server.kurento; -public enum MutedMediaType { +public enum TrackType { ALL, VIDEO, AUDIO; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java index 1194b72c..3a71a25b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java @@ -39,7 +39,6 @@ import org.kurento.client.SdpEndpoint; import org.kurento.client.internal.server.KurentoServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; @@ -49,7 +48,7 @@ import io.openvidu.server.config.InfoHandler; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.MediaOptions; import io.openvidu.server.core.Participant; -import io.openvidu.server.kurento.MutedMediaType; +import io.openvidu.server.kurento.TrackType; import io.openvidu.server.kurento.endpoint.KmsEvent; import io.openvidu.server.kurento.endpoint.MediaEndpoint; import io.openvidu.server.kurento.endpoint.PublisherEndpoint; @@ -60,8 +59,7 @@ public class KurentoParticipant extends Participant { private static final Logger log = LoggerFactory.getLogger(KurentoParticipant.class); - @Autowired - protected OpenviduConfig openviduConfig; + private OpenviduConfig openviduConfig; private InfoHandler infoHandler; private CallDetailRecord CDR; @@ -78,12 +76,14 @@ public class KurentoParticipant extends Participant { private final ConcurrentMap subscribers = new ConcurrentHashMap(); public KurentoParticipant(Participant participant, KurentoSession kurentoSession, MediaPipeline pipeline, - InfoHandler infoHandler, CallDetailRecord CDR) { + InfoHandler infoHandler, CallDetailRecord CDR, OpenviduConfig openviduConfig) { super(participant.getParticipantPrivateId(), participant.getParticipantPublicId(), participant.getToken(), participant.getClientMetadata()); + this.openviduConfig = openviduConfig; this.session = kurentoSession; this.pipeline = pipeline; - this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline); + this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline, + this.openviduConfig); for (Participant other : session.getParticipants()) { if (!other.getParticipantPublicId().equals(this.getParticipantPublicId())) { @@ -108,6 +108,9 @@ public class KurentoParticipant extends Participant { this.publisher.getEndpoint().addTag("name", publisherStreamId); addEndpointListeners(this.publisher); + // Remove streamId from publisher's map + this.session.publishedStreamIds.putIfAbsent(this.getPublisherStreamId(), this.getParticipantPrivateId()); + CDR.recordNewPublisher(this, this.session.getSessionId(), mediaOptions); } @@ -247,7 +250,8 @@ public class KurentoParticipant extends Participant { log.info("PARTICIPANT {}: unpublishing media stream from room {}", this.getParticipantPublicId(), this.session.getSessionId()); releasePublisherEndpoint(reason); - this.publisher = new PublisherEndpoint(webParticipant, this, this.getParticipantPublicId(), pipeline); + this.publisher = new PublisherEndpoint(webParticipant, this, this.getParticipantPublicId(), pipeline, + this.openviduConfig); log.info("PARTICIPANT {}: released publisher endpoint and left it initialized (ready for future streaming)", this.getParticipantPublicId()); } @@ -300,7 +304,7 @@ public class KurentoParticipant extends Participant { throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, "Unable to create subscriber endpoint"); } - String subscriberStreamId = this.getParticipantPublicId() + "_" + kSender.getPublisherStremId(); + String subscriberStreamId = this.getParticipantPublicId() + "_" + kSender.getPublisherStreamId(); subscriber.getEndpoint().addTag("name", subscriberStreamId); @@ -350,54 +354,12 @@ public class KurentoParticipant extends Participant { } } - public void mutePublishedMedia(MutedMediaType muteType) { - if (muteType == null) { - throw new OpenViduException(Code.MEDIA_MUTE_ERROR_CODE, "Mute type cannot be null"); - } - this.getPublisher().mute(muteType); + public void mutePublishedMedia(TrackType trackType) { + this.getPublisher().mute(trackType); } - public void unmutePublishedMedia() { - if (this.getPublisher().getMuteType() == null) { - log.warn("PARTICIPANT {}: Trying to unmute published media. " + "But media is not muted.", - this.getParticipantPublicId()); - } else { - this.getPublisher().unmute(); - } - } - - public void muteSubscribedMedia(Participant sender, MutedMediaType muteType) { - if (muteType == null) { - throw new OpenViduException(Code.MEDIA_MUTE_ERROR_CODE, "Mute type cannot be null"); - } - String senderName = sender.getParticipantPublicId(); - SubscriberEndpoint subscriberEndpoint = subscribers.get(senderName); - if (subscriberEndpoint == null || subscriberEndpoint.getEndpoint() == null) { - log.warn("PARTICIPANT {}: Trying to mute incoming media from user {}. " - + "But there is no such subscriber endpoint.", this.getParticipantPublicId(), senderName); - } else { - log.debug("PARTICIPANT {}: Mute subscriber endpoint linked to user {}", this.getParticipantPublicId(), - senderName); - subscriberEndpoint.mute(muteType); - } - } - - public void unmuteSubscribedMedia(Participant sender) { - String senderName = sender.getParticipantPublicId(); - SubscriberEndpoint subscriberEndpoint = subscribers.get(senderName); - if (subscriberEndpoint == null || subscriberEndpoint.getEndpoint() == null) { - log.warn("PARTICIPANT {}: Trying to unmute incoming media from user {}. " - + "But there is no such subscriber endpoint.", this.getParticipantPublicId(), senderName); - } else { - if (subscriberEndpoint.getMuteType() == null) { - log.warn("PARTICIPANT {}: Trying to unmute incoming media from user {}. " + "But media is not muted.", - this.getParticipantPublicId(), senderName); - } else { - log.debug("PARTICIPANT {}: Unmute subscriber endpoint linked to user {}", this.getParticipantPublicId(), - senderName); - subscriberEndpoint.unmute(); - } - } + public void unmutePublishedMedia(TrackType trackType) { + this.getPublisher().unmute(trackType); } public void close(String reason) { @@ -433,7 +395,8 @@ public class KurentoParticipant extends Participant { */ public SubscriberEndpoint getNewOrExistingSubscriber(String senderPublicId) { - SubscriberEndpoint sendingEndpoint = new SubscriberEndpoint(webParticipant, this, senderPublicId, pipeline); + SubscriberEndpoint sendingEndpoint = new SubscriberEndpoint(webParticipant, this, senderPublicId, pipeline, + this.openviduConfig); SubscriberEndpoint existingSendingEndpoint = this.subscribers.putIfAbsent(senderPublicId, sendingEndpoint); if (existingSendingEndpoint != null) { @@ -468,6 +431,10 @@ public class KurentoParticipant extends Participant { private void releasePublisherEndpoint(String reason) { if (publisher != null && publisher.getEndpoint() != null) { + + // Store streamId from publisher's map + this.session.publishedStreamIds.remove(this.getPublisherStreamId()); + publisher.unregisterErrorListeners(); for (MediaElement el : publisher.getMediaElements()) { releaseElement(getParticipantPublicId(), el); @@ -695,7 +662,7 @@ public class KurentoParticipant extends Participant { } @Override - public String getPublisherStremId() { + public String getPublisherStreamId() { return this.publisher.getEndpoint().getTag("name"); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java index 4d88122a..15ebec7a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java @@ -25,7 +25,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import java.util.stream.Collectors; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -43,10 +42,9 @@ import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; import io.openvidu.java.client.SessionProperties; import io.openvidu.server.cdr.CallDetailRecord; +import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; -import io.openvidu.server.kurento.endpoint.PublisherEndpoint; -import io.openvidu.server.kurento.endpoint.SubscriberEndpoint; /** * @author Pablo Fuente (pablofuenteperez@gmail.com) @@ -56,6 +54,8 @@ public class KurentoSession implements Session { private final static Logger log = LoggerFactory.getLogger(Session.class); public static final int ASYNC_LATCH_TIMEOUT = 30; + private OpenviduConfig openviduConfig; + private final ConcurrentMap participants = new ConcurrentHashMap<>(); private String sessionId; private SessionProperties sessionProperties; @@ -78,14 +78,18 @@ public class KurentoSession implements Session { private CallDetailRecord CDR; + public final ConcurrentHashMap publishedStreamIds = new ConcurrentHashMap<>(); + public KurentoSession(String sessionId, SessionProperties sessionProperties, KurentoClient kurentoClient, - KurentoSessionEventsHandler kurentoSessionHandler, boolean destroyKurentoClient, CallDetailRecord CDR) { + KurentoSessionEventsHandler kurentoSessionHandler, boolean destroyKurentoClient, CallDetailRecord CDR, + OpenviduConfig openviduConfig) { this.sessionId = sessionId; this.sessionProperties = sessionProperties; this.kurentoClient = kurentoClient; this.destroyKurentoClient = destroyKurentoClient; this.kurentoSessionHandler = kurentoSessionHandler; this.CDR = CDR; + this.openviduConfig = openviduConfig; log.debug("New SESSION instance with id '{}'", sessionId); } @@ -105,7 +109,7 @@ public class KurentoSession implements Session { createPipeline(); KurentoParticipant kurentoParticipant = new KurentoParticipant(participant, this, getPipeline(), - kurentoSessionHandler.getInfoHandler(), this.CDR); + kurentoSessionHandler.getInfoHandler(), this.CDR, this.openviduConfig); participants.put(participant.getParticipantPrivateId(), kurentoParticipant); filterStates.forEach((filterId, state) -> { @@ -199,11 +203,6 @@ public class KurentoSession implements Session { return null; } - public Set getAllSubscribersForPublisher(PublisherEndpoint publisher) { - return this.participants.values().stream().flatMap(kp -> kp.getConnectedSubscribedEndpoints(publisher).stream()) - .collect(Collectors.toSet()); - } - @Override public boolean close(String reason) { if (!closed) { @@ -392,4 +391,8 @@ public class KurentoSession implements Session { return json; } + public String getParticipantPrivateIdFromStreamId(String streamId) { + return this.publishedStreamIds.get(streamId); + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java index 8956910d..019b6af3 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java @@ -17,7 +17,9 @@ package io.openvidu.server.kurento.core; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import org.kurento.client.IceCandidate; @@ -36,20 +38,20 @@ import com.google.gson.JsonSyntaxException; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; -import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.SessionProperties; +import io.openvidu.server.core.MediaOptions; +import io.openvidu.server.core.Participant; +import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionManager; import io.openvidu.server.kurento.KurentoClientProvider; import io.openvidu.server.kurento.KurentoClientSessionInfo; import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo; import io.openvidu.server.kurento.endpoint.SdpType; import io.openvidu.server.rpc.RpcHandler; -import io.openvidu.server.core.MediaOptions; -import io.openvidu.server.core.Participant; -import io.openvidu.server.core.Session; public class KurentoSessionManager extends SessionManager { @@ -105,7 +107,8 @@ public class KurentoSessionManager extends SessionManager { } @Override - public synchronized void leaveRoom(Participant participant, Integer transactionId, String reason, boolean closeWebSocket) { + public synchronized void leaveRoom(Participant participant, Integer transactionId, String reason, + boolean closeWebSocket) { log.debug("Request [LEAVE_ROOM] ({})", participant.getParticipantPublicId()); KurentoParticipant kParticipant = (KurentoParticipant) participant; @@ -171,8 +174,8 @@ public class KurentoSessionManager extends SessionManager { log.info("Last participant left. Stopping recording for session {}", sessionId); recordingService.stopRecording(session); - evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID) - .getParticipantPrivateId(), "EVICT_RECORDER"); + evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, + null, "EVICT_RECORDER"); } // Finally close websocket session if required @@ -261,13 +264,13 @@ public class KurentoSessionManager extends SessionManager { participants = kurentoParticipant.getSession().getParticipants(); if (sdpAnswer != null) { - sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStremId(), session.getSessionId(), + sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), session.getSessionId(), mediaOptions, sdpAnswer, participants, transactionId, null); } } @Override - public void unpublishVideo(Participant participant, Integer transactionId, String reason) { + public void unpublishVideo(Participant participant, Participant moderator, Integer transactionId, String reason) { try { KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoSession session = kParticipant.getSession(); @@ -282,11 +285,12 @@ public class KurentoSessionManager extends SessionManager { Set participants = session.getParticipants(); - sessionEventsHandler.onUnpublishMedia(participant, participants, transactionId, null, reason); + sessionEventsHandler.onUnpublishMedia(participant, participants, moderator, transactionId, null, reason); } catch (OpenViduException e) { log.warn("PARTICIPANT {}: Error unpublishing media", participant.getParticipantPublicId(), e); - sessionEventsHandler.onUnpublishMedia(participant, null, transactionId, e, ""); + sessionEventsHandler.onUnpublishMedia(participant, new HashSet<>(Arrays.asList(participant)), moderator, + transactionId, e, ""); } } @@ -327,10 +331,10 @@ public class KurentoSessionManager extends SessionManager { } } catch (OpenViduException e) { log.error("PARTICIPANT {}: Error subscribing to {}", participant.getParticipantPublicId(), senderName, e); - sessionEventsHandler.onSubscribe(participant, session, senderName, null, transactionId, e); + sessionEventsHandler.onSubscribe(participant, session, null, transactionId, e); } if (sdpAnswer != null) { - sessionEventsHandler.onSubscribe(participant, session, senderName, sdpAnswer, transactionId, null); + sessionEventsHandler.onSubscribe(participant, session, sdpAnswer, transactionId, null); } } @@ -353,7 +357,7 @@ public class KurentoSessionManager extends SessionManager { kParticipant.cancelReceivingMedia(senderName, "unsubscribe"); - sessionEventsHandler.onUnsubscribe(participant, senderName, transactionId, null); + sessionEventsHandler.onUnsubscribe(participant, transactionId, null); } @Override @@ -373,7 +377,7 @@ public class KurentoSessionManager extends SessionManager { public void streamPropertyChanged(Participant participant, Integer transactionId, String streamId, String property, JsonElement newValue, String reason) { KurentoParticipant kParticipant = (KurentoParticipant) participant; - streamId = kParticipant.getPublisherStremId(); + streamId = kParticipant.getPublisherStreamId(); MediaOptions streamProperties = kParticipant.getPublisherMediaOptions(); Boolean hasAudio = streamProperties.hasAudio(); @@ -440,7 +444,7 @@ public class KurentoSessionManager extends SessionManager { } KurentoClient kurentoClient = kcProvider.getKurentoClient(kcSessionInfo); session = new KurentoSession(sessionId, sessionProperties, kurentoClient, kurentoSessionEventsHandler, - kcProvider.destroyWhenUnused(), this.CDR); + kcProvider.destroyWhenUnused(), this.CDR, this.openviduConfig); KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session); if (oldSession != null) { @@ -456,19 +460,25 @@ public class KurentoSessionManager extends SessionManager { sessionEventsHandler.onSessionCreated(sessionId); } - /** - * Application-originated request to remove a participant from a session.
- * Side effects: The session event handler should notify the - * participant that she has been evicted. Should also send notifications to all - * other participants about the one that's just been evicted. - * - */ @Override - public void evictParticipant(String participantPrivateId, String reason) throws OpenViduException { - Participant participant = this.getParticipant(participantPrivateId); - this.leaveRoom(participant, null, reason, false); - sessionEventsHandler.onParticipantEvicted(participant, reason); - sessionEventsHandler.closeRpcSession(participantPrivateId); + public void evictParticipant(Participant evictedParticipant, Participant moderator, Integer transactionId, + String reason) throws OpenViduException { + if (evictedParticipant != null) { + KurentoParticipant kParticipant = (KurentoParticipant) evictedParticipant; + Set participants = kParticipant.getSession().getParticipants(); + this.leaveRoom(kParticipant, null, reason, false); + this.sessionEventsHandler.onForceDisconnect(moderator, evictedParticipant, participants, transactionId, + null, reason); + sessionEventsHandler.closeRpcSession(evictedParticipant.getParticipantPrivateId()); + } else { + if (moderator != null && transactionId != null) { + this.sessionEventsHandler.onForceDisconnect(moderator, evictedParticipant, + new HashSet<>(Arrays.asList(moderator)), transactionId, + new OpenViduException(Code.USER_NOT_FOUND_ERROR_CODE, + "Connection not found when calling 'forceDisconnect'"), + ""); + } + } } @Override @@ -509,4 +519,21 @@ public class KurentoSessionManager extends SessionManager { typeOfVideo, frameRate, videoDimensions, doLoopback); } + @Override + public boolean unpublishStream(Session session, String streamId, Participant moderator, Integer transactionId, + String reason) { + String participantPrivateId = ((KurentoSession) session).getParticipantPrivateIdFromStreamId(streamId); + if (participantPrivateId != null) { + Participant participant = this.getParticipant(participantPrivateId); + if (participant != null) { + this.unpublishVideo(participant, moderator, transactionId, reason); + return true; + } else { + return false; + } + } else { + return false; + } + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java index 471115a2..2d021c39 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java @@ -44,8 +44,8 @@ import org.slf4j.LoggerFactory; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.Participant; -import io.openvidu.server.kurento.MutedMediaType; import io.openvidu.server.kurento.core.KurentoParticipant; /** @@ -58,12 +58,18 @@ import io.openvidu.server.kurento.core.KurentoParticipant; */ public abstract class MediaEndpoint { private static Logger log; + private OpenviduConfig openviduConfig; private boolean web = false; private WebRtcEndpoint webEndpoint = null; private RtpEndpoint endpoint = null; + private final int maxRecvKbps; + private final int minRecvKbps; + private final int maxSendKbps; + private final int minSendKbps; + private KurentoParticipant owner; private String endpointName; @@ -73,8 +79,6 @@ public abstract class MediaEndpoint { private final List receivedCandidateList = new LinkedList(); private LinkedList candidates = new LinkedList(); - private MutedMediaType muteType; - public Map flowInMedia = new ConcurrentHashMap<>(); public Map flowOutMedia = new ConcurrentHashMap<>(); @@ -92,7 +96,7 @@ public abstract class MediaEndpoint { * @param log */ public MediaEndpoint(boolean web, KurentoParticipant owner, String endpointName, MediaPipeline pipeline, - Logger log) { + OpenviduConfig openviduConfig, Logger log) { if (log == null) { MediaEndpoint.log = LoggerFactory.getLogger(MediaEndpoint.class); } else { @@ -102,6 +106,12 @@ public abstract class MediaEndpoint { this.owner = owner; this.setEndpointName(endpointName); this.setMediaPipeline(pipeline); + + this.openviduConfig = openviduConfig; + this.maxRecvKbps = this.openviduConfig.getVideoMaxRecvBandwidth(); + this.minRecvKbps = this.openviduConfig.getVideoMinRecvBandwidth(); + this.maxSendKbps = this.openviduConfig.getVideoMaxSendBandwidth(); + this.minSendKbps = this.openviduConfig.getVideoMinSendBandwidth(); } public boolean isWeb() { @@ -204,50 +214,6 @@ public abstract class MediaEndpoint { unregisterElementErrListener(endpoint, endpointSubscription); } - /** - * Mute the media stream. - * - * @param muteType - * which type of leg to disconnect (audio, video or both) - */ - public abstract void mute(MutedMediaType muteType); - - /** - * Reconnect the muted media leg(s). - */ - public abstract void unmute(); - - public void setMuteType(MutedMediaType muteType) { - this.muteType = muteType; - } - - public MutedMediaType getMuteType() { - return this.muteType; - } - - protected void resolveCurrentMuteType(MutedMediaType newMuteType) { - MutedMediaType prev = this.getMuteType(); - if (prev != null) { - switch (prev) { - case AUDIO: - if (muteType.equals(MutedMediaType.VIDEO)) { - this.setMuteType(MutedMediaType.ALL); - return; - } - break; - case VIDEO: - if (muteType.equals(MutedMediaType.AUDIO)) { - this.setMuteType(MutedMediaType.ALL); - return; - } - break; - case ALL: - return; - } - } - this.setMuteType(newMuteType); - } - /** * Creates the endpoint (RTP or WebRTC) and any other additional elements (if * needed). @@ -265,10 +231,10 @@ public abstract class MediaEndpoint { public void onSuccess(WebRtcEndpoint result) throws Exception { webEndpoint = result; - webEndpoint.setMaxVideoRecvBandwidth(600); - webEndpoint.setMinVideoRecvBandwidth(300); - webEndpoint.setMaxVideoSendBandwidth(600); - webEndpoint.setMinVideoSendBandwidth(300); + webEndpoint.setMaxVideoRecvBandwidth(maxRecvKbps); + webEndpoint.setMinVideoRecvBandwidth(minRecvKbps); + webEndpoint.setMaxVideoSendBandwidth(maxSendKbps); + webEndpoint.setMinVideoSendBandwidth(minSendKbps); endpointLatch.countDown(); log.trace("EP {}: Created a new WebRtcEndpoint", endpointName); diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/PublisherEndpoint.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/PublisherEndpoint.java index 31adeb76..36bd5afa 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/PublisherEndpoint.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/PublisherEndpoint.java @@ -36,8 +36,9 @@ import org.slf4j.LoggerFactory; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.MediaOptions; -import io.openvidu.server.kurento.MutedMediaType; +import io.openvidu.server.kurento.TrackType; import io.openvidu.server.kurento.core.KurentoParticipant; /** @@ -46,6 +47,7 @@ import io.openvidu.server.kurento.core.KurentoParticipant; * @author Radu Tom Vlad */ public class PublisherEndpoint extends MediaEndpoint { + private final static Logger log = LoggerFactory.getLogger(PublisherEndpoint.class); protected MediaOptions mediaOptions; @@ -59,8 +61,8 @@ public class PublisherEndpoint extends MediaEndpoint { private Map elementsErrorSubscriptions = new HashMap(); - public PublisherEndpoint(boolean web, KurentoParticipant owner, String endpointName, MediaPipeline pipeline) { - super(web, owner, endpointName, pipeline, log); + public PublisherEndpoint(boolean web, KurentoParticipant owner, String endpointName, MediaPipeline pipeline, OpenviduConfig openviduConfig) { + super(web, owner, endpointName, pipeline, openviduConfig, log); } @Override @@ -283,8 +285,7 @@ public class PublisherEndpoint extends MediaEndpoint { } } - @Override - public synchronized void mute(MutedMediaType muteType) { + public synchronized void mute(TrackType muteType) { MediaElement sink = passThru; if (!elements.isEmpty()) { String sinkId = elementIds.peekLast(); @@ -308,11 +309,9 @@ public class PublisherEndpoint extends MediaEndpoint { internalSinkDisconnect(this.getEndpoint(), sink, MediaType.VIDEO); break; } - resolveCurrentMuteType(muteType); } - @Override - public synchronized void unmute() { + public synchronized void unmute(TrackType muteType) { MediaElement sink = passThru; if (!elements.isEmpty()) { String sinkId = elementIds.peekLast(); @@ -325,8 +324,17 @@ public class PublisherEndpoint extends MediaEndpoint { } else { log.debug("Will unmute connection of WebRTC and PassThrough (no other elems)"); } - internalSinkConnect(this.getEndpoint(), sink); - setMuteType(null); + switch (muteType) { + case ALL: + internalSinkConnect(this.getEndpoint(), sink); + break; + case AUDIO: + internalSinkConnect(this.getEndpoint(), sink, MediaType.AUDIO); + break; + case VIDEO: + internalSinkConnect(this.getEndpoint(), sink, MediaType.VIDEO); + break; + } } private String getNext(String uid) { @@ -466,7 +474,7 @@ public class PublisherEndpoint extends MediaEndpoint { }); } } - + @Override public PublisherEndpoint getPublisher() { return this; diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/SubscriberEndpoint.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/SubscriberEndpoint.java index f0c82205..11facb7c 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/SubscriberEndpoint.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/SubscriberEndpoint.java @@ -19,12 +19,10 @@ package io.openvidu.server.kurento.endpoint; import org.json.simple.JSONObject; import org.kurento.client.MediaPipeline; -import org.kurento.client.MediaType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.openvidu.client.OpenViduException; -import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.kurento.core.KurentoParticipant; /** @@ -39,8 +37,8 @@ public class SubscriberEndpoint extends MediaEndpoint { private PublisherEndpoint publisher = null; - public SubscriberEndpoint(boolean web, KurentoParticipant owner, String endpointName, MediaPipeline pipeline) { - super(web, owner, endpointName, pipeline, log); + public SubscriberEndpoint(boolean web, KurentoParticipant owner, String endpointName, MediaPipeline pipeline, OpenviduConfig openviduConfig) { + super(web, owner, endpointName, pipeline, openviduConfig, log); } public synchronized String subscribe(String sdpOffer, PublisherEndpoint publisher) { @@ -70,31 +68,6 @@ public class SubscriberEndpoint extends MediaEndpoint { this.publisher = publisher; } - @Override - public synchronized void mute(io.openvidu.server.kurento.MutedMediaType muteType) { - if (this.publisher == null) { - throw new OpenViduException(Code.MEDIA_MUTE_ERROR_CODE, "Publisher endpoint not found"); - } - switch (muteType) { - case ALL: - this.publisher.disconnectFrom(this.getEndpoint()); - break; - case AUDIO: - this.publisher.disconnectFrom(this.getEndpoint(), MediaType.AUDIO); - break; - case VIDEO: - this.publisher.disconnectFrom(this.getEndpoint(), MediaType.VIDEO); - break; - } - resolveCurrentMuteType(muteType); - } - - @Override - public synchronized void unmute() { - this.publisher.connect(this.getEndpoint()); - setMuteType(null); - } - @SuppressWarnings("unchecked") @Override public JSONObject toJSON() { diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java index caf0c7ff..49b5af8c 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java @@ -43,6 +43,7 @@ import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.SessionProperties; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.core.Participant; import io.openvidu.server.core.ParticipantRole; import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionManager; @@ -177,6 +178,46 @@ public class SessionRestController { } } + @RequestMapping(value = "/sessions/{sessionId}/connection/{connectionId}", method = RequestMethod.DELETE) + public ResponseEntity disconnectParticipant(@PathVariable("sessionId") String sessionId, + @PathVariable("connectionId") String participantPublicId) { + Session session = this.sessionManager.getSession(sessionId); + if (session != null) { + Participant participant = session.getParticipantByPublicId(participantPublicId); + if (participant != null) { + this.sessionManager.evictParticipant(participant, null, null, "forceDisconnectByServer"); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @RequestMapping(value = "/sessions/{sessionId}/stream/{streamId}", method = RequestMethod.DELETE) + public ResponseEntity unpublishStream(@PathVariable("sessionId") String sessionId, + @PathVariable("streamId") String streamId) { + Session session = this.sessionManager.getSession(sessionId); + if (session != null) { + if (this.sessionManager.unpublishStream(session, streamId, null, null, "forceUnpublishByServer")) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + /* + * @RequestMapping(value = "/sessions/{sessionId}/stream/{streamId}", method = + * RequestMethod.PUT) public ResponseEntity + * muteMedia(@PathVariable("sessionId") String sessionId, + * + * @PathVariable("streamId") String streamId, @RequestBody Map params) { } + */ + @SuppressWarnings("unchecked") @RequestMapping(value = "/tokens", method = RequestMethod.POST) public ResponseEntity newToken(@RequestBody Map params) { @@ -290,8 +331,9 @@ public class SessionRestController { Recording stoppedRecording = this.recordingService.stopRecording(session); - sessionManager.evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID) - .getParticipantPrivateId(), "EVICT_RECORDER"); + sessionManager.evictParticipant( + session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null, + "EVICT_RECORDER"); return new ResponseEntity<>(stoppedRecording.toJson(), HttpStatus.OK); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java index 22032a99..0382dd60 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java @@ -56,7 +56,6 @@ public class RpcHandler extends DefaultJsonRpcHandler { RpcNotificationService notificationService; private ConcurrentMap webSocketEOFTransportError = new ConcurrentHashMap<>(); - // private ConcurrentMap webSocketBrokenPipeTransportError = new ConcurrentHashMap<>(); @Override public void handleRequest(Transaction transaction, Request request) throws Exception { @@ -126,6 +125,12 @@ public class RpcHandler extends DefaultJsonRpcHandler { case ProtocolElements.STREAMPROPERTYCHANGED_METHOD: streamPropertyChanged(rpcConnection, request); break; + case ProtocolElements.FORCEDISCONNECT_METHOD: + forceDisconnect(rpcConnection, request); + break; + case ProtocolElements.FORCEUNPUBLISH_METHOD: + forceUnpublish(rpcConnection, request); + break; default: log.error("Unrecognized request {}", request); break; @@ -190,36 +195,27 @@ public class RpcHandler extends DefaultJsonRpcHandler { } private void leaveRoom(RpcConnection rpcConnection, Request request) { - - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - - if (sessionId == null) { // null when afterConnectionClosed - log.warn("No session information found for participant with privateId {}. " - + "Using the admin method to evict the user.", participantPrivateId); - leaveRoomAfterConnClosed(participantPrivateId, ""); - } else { - // Sanity check: don't call leaveRoom unless the id checks out - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); - if (participant != null) { - log.info("Participant {} is leaving session {}", participant.getParticipantPublicId(), sessionId); - sessionManager.leaveRoom(participant, request.getId(), "disconnect", true); - log.info("Participant {} has left session {}", participant.getParticipantPublicId(), sessionId); - } else { - log.warn("Participant with private id {} not found in session {}. " - + "Using the admin method to evict the user.", participantPrivateId, sessionId); - leaveRoomAfterConnClosed(participantPrivateId, ""); - } + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "disconnect"); + } catch (OpenViduException e) { + return; } + + sessionManager.leaveRoom(participant, request.getId(), "disconnect", true); + log.info("Participant {} has left session {}", participant.getParticipantPublicId(), + rpcConnection.getSessionId()); } private void publishVideo(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "publish"); + } catch (OpenViduException e) { + return; + } - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); - - if (sessionManager.isPublisherInSession(sessionId, participant)) { + if (sessionManager.isPublisherInSession(rpcConnection.getSessionId(), participant)) { MediaOptions options = sessionManager.generateMediaOptions(request); sessionManager.publishVideo(participant, options, request.getId()); } else { @@ -230,10 +226,12 @@ public class RpcHandler extends DefaultJsonRpcHandler { } private void receiveVideoFrom(RpcConnection rpcConnection, Request request) { - - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "subscribe"); + } catch (OpenViduException e) { + return; + } String senderName = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM); senderName = senderName.substring(0, senderName.indexOf("_")); @@ -243,21 +241,24 @@ public class RpcHandler extends DefaultJsonRpcHandler { } private void unsubscribeFromVideo(RpcConnection rpcConnection, Request request) { - - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "unsubscribe"); + } catch (OpenViduException e) { + return; + } String senderName = getStringParam(request, ProtocolElements.UNSUBSCRIBEFROMVIDEO_SENDER_PARAM); - sessionManager.unsubscribe(participant, senderName, request.getId()); } private void onIceCandidate(RpcConnection rpcConnection, Request request) { - - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "onIceCandidate"); + } catch (OpenViduException e) { + return; + } String endpointName = getStringParam(request, ProtocolElements.ONICECANDIDATE_EPNAME_PARAM); String candidate = getStringParam(request, ProtocolElements.ONICECANDIDATE_CANDIDATE_PARAM); @@ -268,41 +269,88 @@ public class RpcHandler extends DefaultJsonRpcHandler { } private void sendMessage(RpcConnection rpcConnection, Request request) { - - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "signal"); + } catch (OpenViduException e) { + return; + } String message = getStringParam(request, ProtocolElements.SENDMESSAGE_MESSAGE_PARAM); - sessionManager.sendMessage(participant, message, request.getId()); } private void unpublishVideo(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "unpublish"); + } catch (OpenViduException e) { + return; + } - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); - - sessionManager.unpublishVideo(participant, request.getId(), "unpublish"); + sessionManager.unpublishVideo(participant, null, request.getId(), "unpublish"); } - - public void streamPropertyChanged(RpcConnection rpcConnection, Request request) { - String participantPrivateId = rpcConnection.getParticipantPrivateId(); - String sessionId = rpcConnection.getSessionId(); - Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); - + + private void forceDisconnect(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "forceDisconnect"); + } catch (OpenViduException e) { + return; + } + + if (sessionManager.isModeratorInSession(rpcConnection.getSessionId(), participant)) { + String connectionId = getStringParam(request, ProtocolElements.FORCEDISCONNECT_CONNECTIONID_PARAM); + sessionManager.evictParticipant( + sessionManager.getSession(rpcConnection.getSessionId()).getParticipantByPublicId(connectionId), + participant, request.getId(), "forceDisconnectByUser"); + } else { + log.error("Error: participant {} is not a moderator", participant.getParticipantPublicId()); + throw new OpenViduException(Code.USER_UNAUTHORIZED_ERROR_CODE, + "Unable to force disconnect. The user does not have a valid token"); + } + } + + private void forceUnpublish(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "forceUnpublish"); + } catch (OpenViduException e) { + return; + } + + if (sessionManager.isModeratorInSession(rpcConnection.getSessionId(), participant)) { + String streamId = getStringParam(request, ProtocolElements.FORCEUNPUBLISH_STREAMID_PARAM); + sessionManager.unpublishStream(sessionManager.getSession(rpcConnection.getSessionId()), streamId, + participant, request.getId(), "forceUnpublishByUser"); + } else { + log.error("Error: participant {} is not a moderator", participant.getParticipantPublicId()); + throw new OpenViduException(Code.USER_UNAUTHORIZED_ERROR_CODE, + "Unable to force unpublish. The user does not have a valid token"); + } + + } + + private void streamPropertyChanged(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "onStreamPropertyChanged"); + } catch (OpenViduException e) { + return; + } + String streamId = getStringParam(request, ProtocolElements.STREAMPROPERTYCHANGED_STREAMID_PARAM); String property = getStringParam(request, ProtocolElements.STREAMPROPERTYCHANGED_PROPERTY_PARAM); JsonElement newValue = getParam(request, ProtocolElements.STREAMPROPERTYCHANGED_NEWVALUE_PARAM); String reason = getStringParam(request, ProtocolElements.STREAMPROPERTYCHANGED_REASON_PARAM); - + sessionManager.streamPropertyChanged(participant, request.getId(), streamId, property, newValue, reason); } public void leaveRoomAfterConnClosed(String participantPrivateId, String reason) { try { - sessionManager.evictParticipant(participantPrivateId, reason); + sessionManager.evictParticipant(this.sessionManager.getParticipant(participantPrivateId), null, null, + reason); log.info("Evicted participant with privateId {}", participantPrivateId); } catch (OpenViduException e) { log.warn("Unable to evict: {}", e.getMessage()); @@ -360,7 +408,6 @@ public class RpcHandler extends DefaultJsonRpcHandler { if ("IOException".equals(exception.getClass().getSimpleName()) && "Broken pipe".equals(exception.getCause().getMessage())) { log.warn("Parcipant with private id {} unexpectedly closed the websocket", rpcSession.getSessionId()); - // this.webSocketBrokenPipeTransportError.put(rpcSession.getSessionId(), true); } if ("EOFException".equals(exception.getClass().getSimpleName())) { // Store WebSocket connection interrupted exception for this web socket to @@ -402,7 +449,7 @@ public class RpcHandler extends DefaultJsonRpcHandler { } return request.getParams().get(key).getAsBoolean(); } - + public static JsonElement getParam(Request request, String key) { if (request.getParams() == null || request.getParams().get(key) == null) { throw new RuntimeException("Request element '" + key + "' is missing in method '" + request.getMethod() @@ -411,4 +458,33 @@ public class RpcHandler extends DefaultJsonRpcHandler { return request.getParams().get(key); } + private Participant sanityCheckOfSession(RpcConnection rpcConnection, String methodName) throws OpenViduException { + String participantPrivateId = rpcConnection.getParticipantPrivateId(); + String sessionId = rpcConnection.getSessionId(); + String errorMsg; + + if (sessionId == null) { // null when afterConnectionClosed + errorMsg = "No session information found for participant with privateId " + participantPrivateId + + ". Using the admin method to evict the user."; + log.warn(errorMsg); + leaveRoomAfterConnClosed(participantPrivateId, ""); + throw new OpenViduException(Code.GENERIC_ERROR_CODE, errorMsg); + } else { + // Sanity check: don't call RPC method unless the id checks out + Participant participant = sessionManager.getParticipant(sessionId, participantPrivateId); + if (participant != null) { + errorMsg = "Participant " + participant.getParticipantPublicId() + " is calling method '" + methodName + + "' in session " + sessionId; + log.info(errorMsg); + return participant; + } else { + errorMsg = "Participant with private id " + participantPrivateId + " not found in session " + sessionId + + ". Using the admin method to evict the user."; + log.warn(errorMsg); + leaveRoomAfterConnClosed(participantPrivateId, ""); + throw new OpenViduException(Code.GENERIC_ERROR_CODE, errorMsg); + } + } + } + } diff --git a/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 9a6240b5..963698f0 100644 --- a/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -64,6 +64,54 @@ "type": "java.lang.String", "description": "Path to COTURN sqlite database to add and remove TURN user credentials", "defaultValue": "/opt/openvidu/coturn/turndb" + }, + { + "name": "openvidu.streams.video.max-recv-bandwidth", + "type": "java.lang.Integer", + "description": "Maximum video bandwith sent from clients to OpenVidu Server, in kbps. 0 means unconstrained", + "defaultValue": 600 + }, + { + "name": "openvidu.streams.video.min-recv-bandwidth", + "type": "java.lang.Integer", + "description": "Minimum video bandwith sent from clients to OpenVidu Server, in kbps. 0 means unconstrained", + "defaultValue": 300 + }, + { + "name": "openvidu.streams.video.max-send-bandwidth", + "type": "java.lang.Integer", + "description": "Maximum video bandwith sent from OpenVidu Server to clients, in kbps. 0 means unconstrained", + "defaultValue": 600 + }, + { + "name": "openvidu.streams.video.min-send-bandwidth", + "type": "java.lang.Integer", + "description": "Minimum video bandwith sent from OpenVidu Server to clients, in kbps. 0 means unconstrained", + "defaultValue": 300 + }, + { + "name": "coturn.redis.ip", + "type": "java.lang.String", + "description": "Redis IP where OpenVidu Server should connect to store TURN credentials", + "defaultValue": "127.0.0.1" + }, + { + "name": "coturn.redis.dbname", + "type": "java.lang.String", + "description": "Redis database where to store TURN credentials", + "defaultValue": "0" + }, + { + "name": "coturn.redis.password", + "type": "java.lang.String", + "description": "Password to connect OpenVidu Server to Redis database to store TURN credentials", + "defaultValue": "turn" + }, + { + "name": "coturn.redis.connect-timeout", + "type": "java.lang.Integer", + "description": "Timeout in seconds when OpenVidu Server is connecting to Redis database to store TURN credentials", + "defaultValue": 30 } ] } \ No newline at end of file diff --git a/openvidu-server/src/main/resources/application.properties b/openvidu-server/src/main/resources/application.properties index e3d9cf04..955ca47c 100644 --- a/openvidu-server/src/main/resources/application.properties +++ b/openvidu-server/src/main/resources/application.properties @@ -9,13 +9,21 @@ kms.uris=[\"ws://localhost:8888/kurento\"] openvidu.secret: uincBgf9ysUCIo4MNbrfMg5hsX6FYYak openvidu.publicurl: https://172.18.2.38:8443 openvidu.cdr: false + openvidu.recording: false openvidu.recording.path: /home/recordings openvidu.recording.public-access: false openvidu.recording.notification: publisher_moderator -openvidu.recording.custom-layout: -kms.uris=[\"ws://localhost:8888/kurento\"] -coturn.redis.ip=127.0.0.1 -coturn.redis.dbname=0 -coturn.redis.password=turn -coturn.redis.connect-timeout=30 \ No newline at end of file +openvidu.recording.custom-layout: /opt/openvidu/custom-layout + +openvidu.streams.video.max-recv-bandwidth: 600 +openvidu.streams.video.min-recv-bandwidth: 300 +openvidu.streams.video.max-send-bandwidth: 600 +openvidu.streams.video.min-send-bandwidth: 300 + +kms.uris: [\"ws://localhost:8888/kurento\"] + +coturn.redis.ip: 127.0.0.1 +coturn.redis.dbname: 0 +coturn.redis.password: turn +coturn.redis.connect-timeout: 30 diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css index b0ee64f6..4728e295 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css @@ -7,7 +7,7 @@ mat-radio-button:first-child { margin-left: 0; } -#turn-conf-label { +.label { display: block; font-size: 12px; color: rgba(0, 0, 0, 0.54); @@ -15,9 +15,8 @@ mat-radio-button:first-child { margin-bottom: 5px; } -.not-manual { - padding-top: 6px; - padding-bottom: 15px; +#turn-div { + padding-bottom: 1.25em; } #manual-turn-div { @@ -25,4 +24,9 @@ mat-radio-button:first-child { padding: 5px; border: 1px solid #00000026; border-radius: 3px; +} + +#role-div { + padding-top: 6px; + padding-bottom: 15px; } \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html index 38bce2fc..e945fc76 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html @@ -28,8 +28,8 @@ - -
+ +
Auto Freeice @@ -47,9 +47,20 @@
+ + +
+ + SUB + PUB + MOD + +
+ + - + \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts index 5ddb9d3c..189eef69 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts @@ -13,6 +13,7 @@ export class SessionPropertiesDialogComponent { sessionProperties: SessionProperties; turnConf: string; manualTurnConf: RTCIceServer = {}; + participantRole: string; mediaMode = MediaMode; recordingMode = RecordingMode; @@ -23,6 +24,7 @@ export class SessionPropertiesDialogComponent { this.sessionProperties = data.sessionProperties; this.turnConf = data.turnConf; this.manualTurnConf = data.manualTurnConf; + this.participantRole = data.participantRole; } enumToArray(enumerator: any) { diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html index f3fdd6e7..18059538 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html @@ -78,7 +78,7 @@
- - - - @@ -47,6 +47,14 @@ + + + + diff --git a/openvidu-testapp/src/app/components/video/video.component.ts b/openvidu-testapp/src/app/components/video/video.component.ts index 407a598e..1fae7b53 100644 --- a/openvidu-testapp/src/app/components/video/video.component.ts +++ b/openvidu-testapp/src/app/components/video/video.component.ts @@ -450,6 +450,10 @@ export class VideoComponent implements OnInit, OnDestroy { event: 'streamDestroyed', content: e.stream.streamId }); + if (e.reason.indexOf('forceUnpublish') !== -1) { + this.unpublished = !this.unpublished; + this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; + } }); } } else { @@ -609,4 +613,12 @@ export class VideoComponent implements OnInit, OnDestroy { } } + forceUnpublish() { + this.OV.session.forceUnpublish(this.streamManager.stream); + } + + forceDisconnect() { + this.OV.session.forceDisconnect(this.streamManager.stream.connection); + } + }