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.autoplay = true;
if (platform.name === 'Safari' && platform.product === 'iPhone') {
this.videoPreview.setAttribute('playsinline', 'true');
}
if (typeof parentElement === 'string') {
this.htmlParentElementId = parentElement;

View File

@ -77,7 +77,7 @@ export class OpenVidu {
constructor() {
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
(<any>window).onorientationchange = () => {
this.publishers.forEach(publisher => {
@ -276,12 +276,23 @@ export class OpenVidu {
*/
checkSystemRequirements(): number {
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') &&
(browser !== 'Firefox') && (browser !== 'Firefox Mobile') && (browser !== 'Firefox for iOS') &&
// Reject iPhones and iPads if not Safari ('Safari' also covers Ionic 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 !== 'Safari') && (browser !== 'Android Browser')) {
(browser !== 'Android Browser')
) {
return 0;
} else {
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
*/
checkScreenSharingCapabilities(): number {
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')) {
return 0;
} else {
@ -484,7 +502,7 @@ export class OpenVidu {
(platform.name!.indexOf('Firefox') !== -1 && publisherProperties.videoSource === 'window')) {
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);
reject(error);
} else {

View File

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

View File

@ -152,7 +152,7 @@ export class Session implements EventDispatcher {
reject(error);
});
} 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'),
id: ''
};
if (platform.name === 'Safari' && platform.product === 'iPhone') {
this.firstVideoElement.video.setAttribute('playsinline', 'true');
}
this.targetElement = targEl;
this.element = targEl;
}
@ -329,6 +332,11 @@ export class StreamManager implements EventDispatcher {
}
video.autoplay = true;
video.controls = false;
if (platform.name === 'Safari' && platform.product === 'iPhone') {
video.setAttribute('playsinline', 'true');
}
if (!video.id) {
video.id = (this.remote ? 'remote-' : 'local-') + 'video-' + this.stream.streamId;
// 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;
iceServers: RTCIceServer[] | undefined;
mediaStream?: MediaStream;
mode?: string; // sendonly, reconly, sendrecv
mode?: 'sendonly' | 'recvonly' | 'sendrecv';
id?: string;
}
@ -156,18 +156,52 @@ export class WebRtcPeer {
console.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints));
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(<string>localDescription.sdp);
} else {
reject('Local description is not defined');
if (platform.name === 'Safari') {
// Safari, at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental
if (offerAudio) {
this.pc.addTransceiver('audio', {
direction: this.configuration.mode,
});
}
}).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);
if (recordingService.sessionIsBeingRecorded(session.getSessionId())) {
recordingService.stopRecording(session, reason);
recordingService.stopRecording(session, null, reason);
}
return participants;

View File

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

View File

@ -31,6 +31,8 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -87,6 +89,10 @@ public class ComposedRecordingService {
private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> startedRecordings = 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 String IMAGE_TAG;
@ -155,10 +161,24 @@ public class ComposedRecordingService {
return recording;
}
public Recording stopRecording(Session session, String reason) {
Recording recording = this.sessionsRecordings.remove(session.getSessionId());
String containerId = this.sessionsContainers.remove(session.getSessionId());
this.startedRecordings.remove(recording.getId());
public Recording stopRecording(Session session, String recordingId, String reason) {
Recording recording;
String containerId;
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) {
@ -250,7 +270,9 @@ public class ComposedRecordingService {
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
"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;
}
@ -465,8 +487,7 @@ public class ComposedRecordingService {
private boolean isFileFromRecording(File file, String recordingId, String recordingName) {
return (((recordingId + ".info").equals(file.getName()))
|| ((RECORDING_ENTITY_FILE + recordingId).equals(file.getName()))
|| (recordingName + ".mp4").equals(file.getName())
|| (recordingId + ".jpg").equals(file.getName()));
|| (recordingName + ".mp4").equals(file.getName()) || (recordingId + ".jpg").equals(file.getName()));
}
private String getFreeRecordingId(String sessionId, String shortSessionId) {
@ -528,4 +549,19 @@ public class ComposedRecordingService {
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);
}
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()
.name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
@ -368,11 +370,14 @@ public class SessionRestController {
Session session = sessionManager.getSession(recording.getSessionId());
Recording stoppedRecording = this.recordingService.stopRecording(session, "recordingStoppedByServer");
Recording stoppedRecording = this.recordingService.stopRecording(session, recording.getId(),
"recordingStoppedByServer");
sessionManager.evictParticipant(
session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null,
"EVICT_RECORDER");
if (session != null) {
sessionManager.evictParticipant(
session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null,
"EVICT_RECORDER");
}
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 {
openviduURL = 'https://localhost:4443/';
openviduURL = 'https://' + window.location.hostname + ':4443/';
openviduSecret = 'MY_SECRET';
constructor(private router: Router, private openviduParamsService: OpenviduParamsService) { }

View File

@ -229,6 +229,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
})
.catch(error => {
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 {
params: OpenviduParams =
{
openviduUrl: 'https://localhost:4443/',
openviduSecret: 'MY_SECRET'
};
{
openviduUrl: 'https://' + window.location.hostname + ':4443/',
openviduSecret: 'MY_SECRET'
};
newParams$ = new Subject<OpenviduParams>();