pull/154/head
kurento 2018-11-26 10:31:05 +01:00
commit 3e777a9fbc
13 changed files with 194 additions and 69 deletions

View File

@ -202,6 +202,10 @@ export class LocalRecorder {
this.videoPreview.id = this.id; this.videoPreview.id = this.id;
this.videoPreview.autoplay = true; this.videoPreview.autoplay = true;
if (platform.name === 'Safari' && platform.product === 'iPhone') {
this.videoPreview.setAttribute('playsinline', 'true');
}
if (typeof parentElement === 'string') { if (typeof parentElement === 'string') {
this.htmlParentElementId = parentElement; this.htmlParentElementId = parentElement;

View File

@ -77,7 +77,7 @@ export class OpenVidu {
constructor() { constructor() {
console.info("'OpenVidu' initialized"); console.info("'OpenVidu' initialized");
if (platform.name!!.toLowerCase().indexOf('mobile') !== -1) { if (platform.os!!.family === 'iOS' || platform.os!!.family === 'Android') {
// Listen to orientationchange only on mobile browsers // Listen to orientationchange only on mobile browsers
(<any>window).onorientationchange = () => { (<any>window).onorientationchange = () => {
this.publishers.forEach(publisher => { this.publishers.forEach(publisher => {
@ -276,12 +276,23 @@ export class OpenVidu {
*/ */
checkSystemRequirements(): number { checkSystemRequirements(): number {
const browser = platform.name; const browser = platform.name;
const version = platform.version; const family = platform.os!!.family;
const userAgent = !!platform.ua ? platform.ua : navigator.userAgent;
if ((browser !== 'Chrome') && (browser !== 'Chrome Mobile') && // Reject iPhones and iPads if not Safari ('Safari' also covers Ionic for iOS)
(browser !== 'Firefox') && (browser !== 'Firefox Mobile') && (browser !== 'Firefox for iOS') && if (family === 'iOS' && (browser !== 'Safari' || userAgent.indexOf('CriOS') !== -1 || userAgent.indexOf('FxiOS') !== -1)) {
return 0;
}
// Accept: Chrome (desktop and Android), Firefox (desktop and Android), Opera (desktop and Android),
// Safari (OSX and iOS), Ionic (Android and iOS)
if (
(browser !== 'Safari') &&
(browser !== 'Chrome') && (browser !== 'Chrome Mobile') &&
(browser !== 'Firefox') && (browser !== 'Firefox Mobile') &&
(browser !== 'Opera') && (browser !== 'Opera Mobile') && (browser !== 'Opera') && (browser !== 'Opera Mobile') &&
(browser !== 'Safari') && (browser !== 'Android Browser')) { (browser !== 'Android Browser')
) {
return 0; return 0;
} else { } else {
return 1; return 1;
@ -290,11 +301,18 @@ export class OpenVidu {
/** /**
* Checks if the browser supports screen-sharing. Chrome, Firefox and Opera support screen-sharing * Checks if the browser supports screen-sharing. Desktop Chrome, Firefox and Opera support screen-sharing
* @returns 1 if the browser supports screen-sharing, 0 otherwise * @returns 1 if the browser supports screen-sharing, 0 otherwise
*/ */
checkScreenSharingCapabilities(): number { checkScreenSharingCapabilities(): number {
const browser = platform.name; const browser = platform.name;
const family = platform.os!!.family;
// Reject mobile devices
if (family === 'iOS' || family === 'Android' || family === 'Windows Phone') {
return 0;
}
if ((browser !== 'Chrome') && (browser !== 'Firefox') && (browser !== 'Opera')) { if ((browser !== 'Chrome') && (browser !== 'Firefox') && (browser !== 'Opera')) {
return 0; return 0;
} else { } else {
@ -484,7 +502,7 @@ export class OpenVidu {
(platform.name!.indexOf('Firefox') !== -1 && publisherProperties.videoSource === 'window')) { (platform.name!.indexOf('Firefox') !== -1 && publisherProperties.videoSource === 'window')) {
if (platform.name !== 'Chrome' && platform.name!.indexOf('Firefox') === -1 && platform.name !== 'Opera') { if (platform.name !== 'Chrome' && platform.name!.indexOf('Firefox') === -1 && platform.name !== 'Opera') {
const error = new OpenViduError(OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, 'You can only screen share in desktop Chrome and Firefox. Detected browser: ' + platform.name); const error = new OpenViduError(OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, 'You can only screen share in desktop Chrome, Firefox or Opera. Detected browser: ' + platform.name);
console.error(error); console.error(error);
reject(error); reject(error);
} else { } else {

View File

@ -292,6 +292,11 @@ export class Publisher extends StreamManager {
} }
this.videoReference = document.createElement('video'); this.videoReference = document.createElement('video');
if (platform.name === 'Safari' && platform.product === 'iPhone') {
this.videoReference.setAttribute('playsinline', 'true');
}
this.videoReference.srcObject = mediaStream; this.videoReference.srcObject = mediaStream;
this.stream.setMediaStream(mediaStream); this.stream.setMediaStream(mediaStream);
@ -337,8 +342,8 @@ export class Publisher extends StreamManager {
}; };
this.screenShareResizeInterval = setInterval(() => { this.screenShareResizeInterval = setInterval(() => {
const firefoxSettings = mediaStream.getVideoTracks()[0].getSettings(); const firefoxSettings = mediaStream.getVideoTracks()[0].getSettings();
const newWidth = (platform.name === 'Chrome') ? this.videoReference.videoWidth : firefoxSettings.width; const newWidth = (platform.name === 'Chrome' || platform.name === 'Opera') ? this.videoReference.videoWidth : firefoxSettings.width;
const newHeight = (platform.name === 'Chrome') ? this.videoReference.videoHeight : firefoxSettings.height; const newHeight = (platform.name === 'Chrome' || platform.name === 'Opera') ? this.videoReference.videoHeight : firefoxSettings.height;
if (this.stream.isLocalStreamPublished && if (this.stream.isLocalStreamPublished &&
(newWidth !== this.stream.videoDimensions.width || (newWidth !== this.stream.videoDimensions.width ||
newHeight !== this.stream.videoDimensions.height)) { newHeight !== this.stream.videoDimensions.height)) {

View File

@ -152,7 +152,7 @@ export class Session implements EventDispatcher {
reject(error); reject(error);
}); });
} else { } else {
reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' ' + platform.version + ' is not supported in OpenVidu')); reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' for ' + platform.os!!.family + ' is not supported in OpenVidu'));
} }
}); });
} }

View File

@ -112,6 +112,9 @@ export class StreamManager implements EventDispatcher {
video: document.createElement('video'), video: document.createElement('video'),
id: '' id: ''
}; };
if (platform.name === 'Safari' && platform.product === 'iPhone') {
this.firstVideoElement.video.setAttribute('playsinline', 'true');
}
this.targetElement = targEl; this.targetElement = targEl;
this.element = targEl; this.element = targEl;
} }
@ -329,6 +332,11 @@ export class StreamManager implements EventDispatcher {
} }
video.autoplay = true; video.autoplay = true;
video.controls = false; video.controls = false;
if (platform.name === 'Safari' && platform.product === 'iPhone') {
video.setAttribute('playsinline', 'true');
}
if (!video.id) { if (!video.id) {
video.id = (this.remote ? 'remote-' : 'local-') + 'video-' + this.stream.streamId; video.id = (this.remote ? 'remote-' : 'local-') + 'video-' + this.stream.streamId;
// DEPRECATED property: assign once the property id if the user provided a valid targetElement // DEPRECATED property: assign once the property id if the user provided a valid targetElement

View File

@ -29,7 +29,7 @@ export interface WebRtcPeerConfiguration {
onicecandidate: (event) => void; onicecandidate: (event) => void;
iceServers: RTCIceServer[] | undefined; iceServers: RTCIceServer[] | undefined;
mediaStream?: MediaStream; mediaStream?: MediaStream;
mode?: string; // sendonly, reconly, sendrecv mode?: 'sendonly' | 'recvonly' | 'sendrecv';
id?: string; id?: string;
} }
@ -156,18 +156,52 @@ export class WebRtcPeer {
console.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints)); console.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints));
this.pc.createOffer(constraints).then(offer => { if (platform.name === 'Safari') {
console.debug('Created SDP offer'); // Safari, at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental
return this.pc.setLocalDescription(offer); if (offerAudio) {
}).then(() => { this.pc.addTransceiver('audio', {
const localDescription = this.pc.localDescription; direction: this.configuration.mode,
if (!!localDescription) { });
console.debug('Local description set', localDescription.sdp);
resolve(<string>localDescription.sdp);
} else {
reject('Local description is not defined');
} }
}).catch(error => reject(error));
if (offerVideo) {
this.pc.addTransceiver('video', {
direction: this.configuration.mode,
});
}
this.pc
.createOffer()
.then(offer => {
console.debug('Created SDP offer');
return this.pc.setLocalDescription(offer);
})
.then(() => {
const localDescription = this.pc.localDescription;
if (!!localDescription) {
console.debug('Local description set', localDescription.sdp);
resolve(localDescription.sdp);
} else {
reject('Local description is not defined');
}
})
.catch(error => reject(error));
} else {
this.pc.createOffer(constraints).then(offer => {
console.debug('Created SDP offer');
return this.pc.setLocalDescription(offer);
}).then(() => {
const localDescription = this.pc.localDescription;
if (!!localDescription) {
console.debug('Local description set', localDescription.sdp);
resolve(localDescription.sdp);
} else {
reject('Local description is not defined');
}
})
.catch(error => reject(error));
}
}); });
} }

View File

@ -451,7 +451,7 @@ public abstract class SessionManager {
this.closeSessionAndEmptyCollections(session, reason); this.closeSessionAndEmptyCollections(session, reason);
if (recordingService.sessionIsBeingRecorded(session.getSessionId())) { if (recordingService.sessionIsBeingRecorded(session.getSessionId())) {
recordingService.stopRecording(session, reason); recordingService.stopRecording(session, null, reason);
} }
return participants; return participants;

View File

@ -175,14 +175,21 @@ public class KurentoSessionManager extends SessionManager {
showTokens(); showTokens();
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled() } else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID && ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) { .equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
log.info("Last participant left. Stopping recording for session {}", sessionId); // Immediately stop recording when last real participant left if
recordingService.stopRecording(session, reason); // RecordingMode.ALWAYS
evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, log.info("Last participant left. Stopping recording for session {}", sessionId);
null, "EVICT_RECORDER"); recordingService.stopRecording(session, null, reason);
evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null,
null, "EVICT_RECORDER");
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
// a Publisher starts before timeout)
log.info("Last participant left. Starting countdown for stopping recording of session {}", sessionId);
recordingService.initAutomaticRecordingStopThread(session.getSessionId());
}
} }
// Finally close websocket session if required // Finally close websocket session if required
@ -206,14 +213,10 @@ public class KurentoSessionManager extends SessionManager {
* the peer's request by sending it the SDP response (answer or updated offer) * the peer's request by sending it the SDP response (answer or updated offer)
* generated by the WebRTC endpoint on the server. * generated by the WebRTC endpoint on the server.
* *
* @param participant * @param participant Participant publishing video
* Participant publishing video * @param MediaOptions configuration of the stream to publish
* @param MediaOptions * @param transactionId identifier of the Transaction
* configuration of the stream to publish * @throws OpenViduException on error
* @param transactionId
* identifier of the Transaction
* @throws OpenViduException
* on error
*/ */
@Override @Override
public void publishVideo(Participant participant, MediaOptions mediaOptions, Integer transactionId) public void publishVideo(Participant participant, MediaOptions mediaOptions, Integer transactionId)
@ -272,16 +275,29 @@ public class KurentoSessionManager extends SessionManager {
if (this.openviduConfig.isRecordingModuleEnabled() if (this.openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
&& !recordingService.sessionIsBeingRecorded(session.getSessionId())
&& session.getActivePublishers() == 0) { && session.getActivePublishers() == 0) {
// Insecure session recording if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
new Thread(() -> { && !recordingService.sessionIsBeingRecorded(session.getSessionId())) {
recordingService.startRecording(session, // Insecure session recording
new RecordingProperties.Builder().name("") new Thread(() -> {
.recordingLayout(session.getSessionProperties().defaultRecordingLayout()) recordingService.startRecording(session,
.customLayout(session.getSessionProperties().defaultCustomLayout()).build()); new RecordingProperties.Builder().name("")
}).start(); .recordingLayout(session.getSessionProperties().defaultRecordingLayout())
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
}).start();
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())
&& recordingService.sessionIsBeingRecorded(session.getSessionId())) {
// Abort automatic recording stop (user published before timeout)
log.info("Participant {} published before timeout finished. Aborting automatic recording stop",
participant.getParticipantPublicId());
boolean stopAborted = recordingService.abortAutomaticRecordingStopThread(session.getSessionId());
if (stopAborted) {
log.info("Automatic recording stopped succesfully aborted");
} else {
log.info("Automatic recording stopped couldn't be aborted. Recording of session {} has stopped",
session.getSessionId());
}
}
} }
session.newPublisher(participant); session.newPublisher(participant);
@ -458,12 +474,10 @@ public class KurentoSessionManager extends SessionManager {
* Creates a session if it doesn't already exist. The session's id will be * Creates a session if it doesn't already exist. The session's id will be
* indicated by the session info bean. * indicated by the session info bean.
* *
* @param kcSessionInfo * @param kcSessionInfo bean that will be passed to the
* bean that will be passed to the {@link KurentoClientProvider} in * {@link KurentoClientProvider} in order to obtain the
* order to obtain the {@link KurentoClient} that will be used by the * {@link KurentoClient} that will be used by the room
* room * @throws OpenViduException in case of error while creating the session
* @throws OpenViduException
* in case of error while creating the session
*/ */
public void createSession(KurentoClientSessionInfo kcSessionInfo, SessionProperties sessionProperties) public void createSession(KurentoClientSessionInfo kcSessionInfo, SessionProperties sessionProperties)
throws OpenViduException { throws OpenViduException {

View File

@ -31,6 +31,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -87,6 +89,10 @@ public class ComposedRecordingService {
private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>(); private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> startedRecordings = new ConcurrentHashMap<>(); private Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>(); private Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
private final Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
Runtime.getRuntime().availableProcessors());
private final String IMAGE_NAME = "openvidu/openvidu-recording"; private final String IMAGE_NAME = "openvidu/openvidu-recording";
private String IMAGE_TAG; private String IMAGE_TAG;
@ -155,10 +161,24 @@ public class ComposedRecordingService {
return recording; return recording;
} }
public Recording stopRecording(Session session, String reason) { public Recording stopRecording(Session session, String recordingId, String reason) {
Recording recording = this.sessionsRecordings.remove(session.getSessionId()); Recording recording;
String containerId = this.sessionsContainers.remove(session.getSessionId()); String containerId;
this.startedRecordings.remove(recording.getId());
if (session == null) {
log.warn(
"Existing recording {} does not have an active session associated. This usually means the recording"
+ " layout did not join a recorded participant or the recording has been automatically"
+ " stopped after last user left and timeout passed",
recordingId);
recording = this.startedRecordings.remove(recordingId);
containerId = this.sessionsContainers.remove(recording.getSessionId());
this.sessionsRecordings.remove(recording.getSessionId());
} else {
recording = this.sessionsRecordings.remove(session.getSessionId());
containerId = this.sessionsContainers.remove(session.getSessionId());
this.startedRecordings.remove(recording.getId());
}
if (containerId == null) { if (containerId == null) {
@ -250,7 +270,9 @@ public class ComposedRecordingService {
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE, throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
"There was an error generating the metadata report file for the recording"); "There was an error generating the metadata report file for the recording");
} }
this.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); if (session != null) {
this.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
}
} }
return recording; return recording;
} }
@ -465,8 +487,7 @@ public class ComposedRecordingService {
private boolean isFileFromRecording(File file, String recordingId, String recordingName) { private boolean isFileFromRecording(File file, String recordingId, String recordingName) {
return (((recordingId + ".info").equals(file.getName())) return (((recordingId + ".info").equals(file.getName()))
|| ((RECORDING_ENTITY_FILE + recordingId).equals(file.getName())) || ((RECORDING_ENTITY_FILE + recordingId).equals(file.getName()))
|| (recordingName + ".mp4").equals(file.getName()) || (recordingName + ".mp4").equals(file.getName()) || (recordingId + ".jpg").equals(file.getName()));
|| (recordingId + ".jpg").equals(file.getName()));
} }
private String getFreeRecordingId(String sessionId, String shortSessionId) { private String getFreeRecordingId(String sessionId, String shortSessionId) {
@ -528,4 +549,19 @@ public class ComposedRecordingService {
this.IMAGE_TAG = version; this.IMAGE_TAG = version;
} }
public void initAutomaticRecordingStopThread(String sessionId) {
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
this.stopRecording(null, recordingId, "lastParticipantLeft");
this.automaticRecordingStopThreads.remove(sessionId);
}, 2, TimeUnit.MINUTES);
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
}
public boolean abortAutomaticRecordingStopThread(String sessionId) {
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(sessionId);
return future.cancel(false);
}
} }

View File

@ -336,7 +336,9 @@ public class SessionRestController {
recordingLayout = RecordingLayout.valueOf(recordingLayoutString); recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
} }
customLayout = (customLayout == null) ? session.getSessionProperties().defaultCustomLayout() : customLayout; customLayout = (customLayout == null || customLayout.isEmpty())
? session.getSessionProperties().defaultCustomLayout()
: customLayout;
Recording startedRecording = this.recordingService.startRecording(session, new RecordingProperties.Builder() Recording startedRecording = this.recordingService.startRecording(session, new RecordingProperties.Builder()
.name(name).recordingLayout(recordingLayout).customLayout(customLayout).build()); .name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
@ -368,11 +370,14 @@ public class SessionRestController {
Session session = sessionManager.getSession(recording.getSessionId()); Session session = sessionManager.getSession(recording.getSessionId());
Recording stoppedRecording = this.recordingService.stopRecording(session, "recordingStoppedByServer"); Recording stoppedRecording = this.recordingService.stopRecording(session, recording.getId(),
"recordingStoppedByServer");
sessionManager.evictParticipant( if (session != null) {
session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null, sessionManager.evictParticipant(
"EVICT_RECORDER"); session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null,
"EVICT_RECORDER");
}
return new ResponseEntity<>(stoppedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK); return new ResponseEntity<>(stoppedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
} }

View File

@ -9,7 +9,7 @@ import { OpenviduParamsService } from './services/openvidu-params.service';
}) })
export class AppComponent { export class AppComponent {
openviduURL = 'https://localhost:4443/'; openviduURL = 'https://' + window.location.hostname + ':4443/';
openviduSecret = 'MY_SECRET'; openviduSecret = 'MY_SECRET';
constructor(private router: Router, private openviduParamsService: OpenviduParamsService) { } constructor(private router: Router, private openviduParamsService: OpenviduParamsService) { }

View File

@ -229,6 +229,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
}) })
.catch(error => { .catch(error => {
console.log('There was an error connecting to the session:', error.code, error.message); console.log('There was an error connecting to the session:', error.code, error.message);
alert('Error connecting to the session: ' + error.message);
}); });
} }

View File

@ -10,10 +10,10 @@ export interface OpenviduParams {
export class OpenviduParamsService { export class OpenviduParamsService {
params: OpenviduParams = params: OpenviduParams =
{ {
openviduUrl: 'https://localhost:4443/', openviduUrl: 'https://' + window.location.hostname + ':4443/',
openviduSecret: 'MY_SECRET' openviduSecret: 'MY_SECRET'
}; };
newParams$ = new Subject<OpenviduParams>(); newParams$ = new Subject<OpenviduParams>();