Fixing stop of stream in bi-directional screen sharing.

This commit is contained in:
David Négrier 2020-08-21 22:53:17 +02:00
parent f60b02f1dc
commit 91f422d0c3
3 changed files with 98 additions and 34 deletions

View File

@ -1,5 +1,5 @@
import * as SimplePeerNamespace from "simple-peer";
import {DivImportance, layoutManager} from "./LayoutManager"; import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils";
const videoConstraint: boolean|MediaTrackConstraints = { const videoConstraint: boolean|MediaTrackConstraints = {
width: { ideal: 1280 }, width: { ideal: 1280 },
@ -8,7 +8,8 @@ const videoConstraint: boolean|MediaTrackConstraints = {
}; };
type UpdatedLocalStreamCallback = (media: MediaStream) => void; type UpdatedLocalStreamCallback = (media: MediaStream) => void;
type UpdatedScreenSharingCallback = (media: MediaStream) => void; type StartScreenSharingCallback = (media: MediaStream) => void;
type StopScreenSharingCallback = (media: MediaStream) => void;
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only) // TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!) // TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
@ -29,7 +30,8 @@ export class MediaManager {
video: videoConstraint video: videoConstraint
}; };
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>(); updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
updatedScreenSharingCallBacks : Set<UpdatedScreenSharingCallback> = new Set<UpdatedScreenSharingCallback>(); startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
constructor() { constructor() {
@ -87,9 +89,14 @@ export class MediaManager {
this.updatedLocalStreamCallBacks.add(callback); this.updatedLocalStreamCallBacks.add(callback);
} }
public onUpdateScreenSharing(callback: UpdatedScreenSharingCallback): void { public onStartScreenSharing(callback: StartScreenSharingCallback): void {
this.updatedScreenSharingCallBacks.add(callback); this.startScreenSharingCallBacks.add(callback);
}
public onStopScreenSharing(callback: StopScreenSharingCallback): void {
this.stopScreenSharingCallBacks.add(callback);
} }
removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void { removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
@ -102,8 +109,14 @@ export class MediaManager {
} }
} }
private triggerUpdatedScreenSharingCallbacks(stream: MediaStream): void { private triggerStartedScreenSharingCallbacks(stream: MediaStream): void {
for (const callback of this.updatedScreenSharingCallBacks) { for (const callback of this.startScreenSharingCallBacks) {
callback(stream);
}
}
private triggerStoppedScreenSharingCallbacks(stream: MediaStream): void {
for (const callback of this.stopScreenSharingCallBacks) {
callback(stream); callback(stream);
} }
} }
@ -164,20 +177,26 @@ export class MediaManager {
this.monitorClose.style.display = "none"; this.monitorClose.style.display = "none";
this.monitor.style.display = "block"; this.monitor.style.display = "block";
this.getScreenMedia().then((stream) => { this.getScreenMedia().then((stream) => {
this.triggerUpdatedScreenSharingCallbacks(stream); this.triggerStartedScreenSharingCallbacks(stream);
}); });
} }
private disableScreenSharing() { private disableScreenSharing() {
this.monitorClose.style.display = "block"; this.monitorClose.style.display = "block";
this.monitor.style.display = "none"; this.monitor.style.display = "none";
this.removeActiveScreenSharingVideo('me');
this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => { this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => {
track.stop(); track.stop();
}); });
this.localScreenCapture = null; if (this.localScreenCapture === null) {
console.warn('Weird: trying to remove a screen sharing that is not enabled');
return;
}
const localScreenCapture = this.localScreenCapture;
this.getCamera().then((stream) => { this.getCamera().then((stream) => {
this.triggerUpdatedScreenSharingCallbacks(stream); this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
}); });
this.localScreenCapture = null;
} }
//get screen //get screen
@ -194,6 +213,9 @@ export class MediaManager {
}; };
} }
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = stream;
return stream; return stream;
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
@ -306,8 +328,8 @@ export class MediaManager {
* *
* @param userId * @param userId
*/ */
addScreenSharingActiveVideo(userId : string){ addScreenSharingActiveVideo(userId : string, divImportance: DivImportance = DivImportance.Important){
this.webrtcInAudio.play(); //this.webrtcInAudio.play();
userId = `screen-sharing-${userId}`; userId = `screen-sharing-${userId}`;
const html = ` const html = `
@ -316,7 +338,7 @@ export class MediaManager {
</div> </div>
`; `;
layoutManager.add(DivImportance.Important, userId, html); layoutManager.add(divImportance, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
} }
@ -389,6 +411,12 @@ export class MediaManager {
remoteVideo.srcObject = stream; remoteVideo.srcObject = stream;
} }
addStreamRemoteScreenSharing(userId : string, stream : MediaStream){ addStreamRemoteScreenSharing(userId : string, stream : MediaStream){
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
const remoteVideo = this.remoteVideo.get(`screen-sharing-${userId}`);
if (remoteVideo === undefined) {
this.addScreenSharingActiveVideo(userId);
}
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream); this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream);
} }

View File

@ -8,6 +8,11 @@ const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
*/ */
export class ScreenSharingPeer extends Peer { export class ScreenSharingPeer extends Peer {
/**
* Whether this connection is currently receiving a video stream from a remote user.
*/
private isReceivingStream:boolean = false;
constructor(private userId: string, initiator: boolean, private connection: Connection) { constructor(private userId: string, initiator: boolean, private connection: Connection) {
super({ super({
initiator: initiator ? initiator : false, initiator: initiator ? initiator : false,
@ -35,13 +40,20 @@ export class ScreenSharingPeer extends Peer {
this.stream(stream); this.stream(stream);
}); });
/*this.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
});*/
this.on('close', () => { this.on('close', () => {
this.destroy(); this.destroy();
}); });
this.on('data', (chunk: Buffer) => {
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
// It seems there is no native way to detect that.
const message = JSON.parse(chunk.toString('utf8'));
if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection');
}
mediaManager.removeActiveScreenSharingVideo(this.userId);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on('error', (err: any) => {
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
@ -74,11 +86,17 @@ export class ScreenSharingPeer extends Peer {
console.log(`stream => ${this.userId} => `, stream); console.log(`stream => ${this.userId} => `, stream);
if(!stream){ if(!stream){
mediaManager.removeActiveScreenSharingVideo(this.userId); mediaManager.removeActiveScreenSharingVideo(this.userId);
this.isReceivingStream = false;
} else { } else {
mediaManager.addStreamRemoteScreenSharing(this.userId, stream); mediaManager.addStreamRemoteScreenSharing(this.userId, stream);
this.isReceivingStream = true;
} }
} }
public isReceivingScreenSharingStream(): boolean {
return this.isReceivingStream;
}
public destroy(error?: Error): void { public destroy(error?: Error): void {
try { try {
mediaManager.removeActiveScreenSharingVideo(this.userId); mediaManager.removeActiveScreenSharingVideo(this.userId);
@ -98,9 +116,12 @@ export class ScreenSharingPeer extends Peer {
return; return;
} }
for (const track of localScreenCapture.getTracks()) { this.addStream(localScreenCapture);
this.addTrack(track, localScreenCapture);
}
return; return;
} }
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream);
this.write(new Buffer(JSON.stringify({streamEnded: true})));
}
} }

View File

@ -34,6 +34,7 @@ export class SimplePeer {
private PeerConnectionArray: Map<string, VideoPeer> = new Map<string, VideoPeer>(); private PeerConnectionArray: Map<string, VideoPeer> = new Map<string, VideoPeer>();
private readonly sendLocalVideoStreamCallback: (media: MediaStream) => void; private readonly sendLocalVideoStreamCallback: (media: MediaStream) => void;
private readonly sendLocalScreenSharingStreamCallback: (media: MediaStream) => void; private readonly sendLocalScreenSharingStreamCallback: (media: MediaStream) => void;
private readonly stopLocalScreenSharingStreamCallback: (media: MediaStream) => void;
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
@ -42,8 +43,10 @@ export class SimplePeer {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this); this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback); mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
mediaManager.onUpdateScreenSharing(this.sendLocalScreenSharingStreamCallback); mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
this.initialise(); this.initialise();
} }
@ -332,14 +335,22 @@ export class SimplePeer {
* Triggered locally when clicking on the screen sharing button * Triggered locally when clicking on the screen sharing button
*/ */
public sendLocalScreenSharingStream() { public sendLocalScreenSharingStream() {
if (mediaManager.localScreenCapture) { if (!mediaManager.localScreenCapture) {
for (const user of this.Users) { console.error('Could not find localScreenCapture to share')
this.sendLocalScreenSharingStreamToUser(user.userId); return;
} }
} else {
for (const user of this.Users) { for (const user of this.Users) {
this.stopLocalScreenSharingStreamToUser(user.userId); this.sendLocalScreenSharingStreamToUser(user.userId);
} }
}
/**
* Triggered locally when clicking on the screen sharing button
*/
public stopLocalScreenSharingStream(stream: MediaStream) {
for (const user of this.Users) {
this.stopLocalScreenSharingStreamToUser(user.userId, stream);
} }
} }
@ -360,17 +371,21 @@ export class SimplePeer {
} }
} }
private stopLocalScreenSharingStreamToUser(userId: string): void { private stopLocalScreenSharingStreamToUser(userId: string, stream: MediaStream): void {
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnectionScreenSharing) { if (!PeerConnectionScreenSharing) {
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found') throw new Error('Weird, screen sharing connection to user ' + userId + 'not found')
} }
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing); console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
// FIXME: maybe we don't want to destroy the connexion if it is used in the other way around!
// FIXME: maybe we don't want to destroy the connexion if it is used in the other way around! // Stop sending stream and close peer connection if peer is not sending stream too
// FIXME: maybe we don't want to destroy the connexion if it is used in the other way around! PeerConnectionScreenSharing.stopPushingScreenSharingToRemoteUser(stream);
PeerConnectionScreenSharing.destroy();
this.PeerScreenSharingConnectionArray.delete(userId); if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
PeerConnectionScreenSharing.destroy();
this.PeerScreenSharingConnectionArray.delete(userId);
}
} }
} }