Merge branch 'develop'
|
|
@ -1,4 +1,19 @@
|
||||||
module.exports =
|
module.exports =
|
||||||
{
|
{
|
||||||
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi'
|
chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi',
|
||||||
|
loginEnabled : false,
|
||||||
|
turnServers : [
|
||||||
|
{
|
||||||
|
urls : [
|
||||||
|
'turn:example.com:443?transport=tcp'
|
||||||
|
],
|
||||||
|
username : 'example',
|
||||||
|
credential : 'example'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
requestTimeout : 10000,
|
||||||
|
transportOptions :
|
||||||
|
{
|
||||||
|
tcp : true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ gulp.task('livebrowser', (done) =>
|
||||||
{
|
{
|
||||||
open : 'external',
|
open : 'external',
|
||||||
host : config.domain,
|
host : config.domain,
|
||||||
|
port : 3000,
|
||||||
server :
|
server :
|
||||||
{
|
{
|
||||||
baseDir : OUTPUT_DIR
|
baseDir : OUTPUT_DIR
|
||||||
|
|
@ -226,6 +227,7 @@ gulp.task('browser', (done) =>
|
||||||
{
|
{
|
||||||
open : 'external',
|
open : 'external',
|
||||||
host : config.domain,
|
host : config.domain,
|
||||||
|
port : 3000,
|
||||||
server :
|
server :
|
||||||
{
|
{
|
||||||
baseDir : OUTPUT_DIR
|
baseDir : OUTPUT_DIR
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,25 @@ import { getProtooUrl } from './urlFactory';
|
||||||
import * as cookiesManager from './cookiesManager';
|
import * as cookiesManager from './cookiesManager';
|
||||||
import * as requestActions from './redux/requestActions';
|
import * as requestActions from './redux/requestActions';
|
||||||
import * as stateActions from './redux/stateActions';
|
import * as stateActions from './redux/stateActions';
|
||||||
|
import {
|
||||||
|
turnServers,
|
||||||
|
requestTimeout,
|
||||||
|
transportOptions
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
const logger = new Logger('RoomClient');
|
const logger = new Logger('RoomClient');
|
||||||
|
|
||||||
const ROOM_OPTIONS =
|
const ROOM_OPTIONS =
|
||||||
{
|
{
|
||||||
requestTimeout : 10000,
|
requestTimeout : requestTimeout,
|
||||||
transportOptions :
|
transportOptions : transportOptions,
|
||||||
{
|
turnServers : turnServers
|
||||||
tcp : true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIDEO_CONSTRAINS =
|
const VIDEO_CONSTRAINS =
|
||||||
{
|
{
|
||||||
qvga : { width: { ideal: 320 }, height: { ideal: 240 } },
|
width : { ideal: 1280 },
|
||||||
vga : { width: { ideal: 640 }, height: { ideal: 480 } },
|
aspectRatio : 1.334
|
||||||
hd : { width: { ideal: 1280 }, height: { ideal: 720 } }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RoomClient
|
export default class RoomClient
|
||||||
|
|
@ -37,6 +39,9 @@ export default class RoomClient
|
||||||
const protooUrl = getProtooUrl(peerName, roomId);
|
const protooUrl = getProtooUrl(peerName, roomId);
|
||||||
const protooTransport = new protooClient.WebSocketTransport(protooUrl);
|
const protooTransport = new protooClient.WebSocketTransport(protooUrl);
|
||||||
|
|
||||||
|
// window element to external login site
|
||||||
|
this._loginWindow;
|
||||||
|
|
||||||
// Closed flag.
|
// Closed flag.
|
||||||
this._closed = false;
|
this._closed = false;
|
||||||
|
|
||||||
|
|
@ -52,6 +57,9 @@ export default class RoomClient
|
||||||
// Redux store getState function.
|
// Redux store getState function.
|
||||||
this._getState = getState;
|
this._getState = getState;
|
||||||
|
|
||||||
|
// This device
|
||||||
|
this._device = device;
|
||||||
|
|
||||||
// My peer name.
|
// My peer name.
|
||||||
this._peerName = peerName;
|
this._peerName = peerName;
|
||||||
|
|
||||||
|
|
@ -60,6 +68,7 @@ export default class RoomClient
|
||||||
|
|
||||||
// mediasoup-client Room instance.
|
// mediasoup-client Room instance.
|
||||||
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
|
this._room = new mediasoupClient.Room(ROOM_OPTIONS);
|
||||||
|
this._room.roomId = roomId;
|
||||||
|
|
||||||
// Transport for sending.
|
// Transport for sending.
|
||||||
this._sendTransport = null;
|
this._sendTransport = null;
|
||||||
|
|
@ -77,6 +86,8 @@ export default class RoomClient
|
||||||
// @type {Map<String, MediaDeviceInfos>}
|
// @type {Map<String, MediaDeviceInfos>}
|
||||||
this._webcams = new Map();
|
this._webcams = new Map();
|
||||||
|
|
||||||
|
this._audioDevices = new Map();
|
||||||
|
|
||||||
// Local Webcam. Object with:
|
// Local Webcam. Object with:
|
||||||
// - {MediaDeviceInfo} [device]
|
// - {MediaDeviceInfo} [device]
|
||||||
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
|
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
|
||||||
|
|
@ -85,6 +96,10 @@ export default class RoomClient
|
||||||
resolution : 'hd'
|
resolution : 'hd'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._audioDevice = {
|
||||||
|
device : null
|
||||||
|
};
|
||||||
|
|
||||||
this._screenSharing = ScreenShare.create();
|
this._screenSharing = ScreenShare.create();
|
||||||
|
|
||||||
this._screenSharingProducer = null;
|
this._screenSharingProducer = null;
|
||||||
|
|
@ -111,6 +126,20 @@ export default class RoomClient
|
||||||
this._dispatch(stateActions.setRoomState('closed'));
|
this._dispatch(stateActions.setRoomState('closed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login()
|
||||||
|
{
|
||||||
|
this._dispatch(stateActions.setLoginInProgress(true));
|
||||||
|
|
||||||
|
const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`;
|
||||||
|
|
||||||
|
this._loginWindow = window.open(url, 'loginWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLoginWindow()
|
||||||
|
{
|
||||||
|
this._loginWindow.close();
|
||||||
|
}
|
||||||
|
|
||||||
changeDisplayName(displayName)
|
changeDisplayName(displayName)
|
||||||
{
|
{
|
||||||
logger.debug('changeDisplayName() [displayName:"%s"]', displayName);
|
logger.debug('changeDisplayName() [displayName:"%s"]', displayName);
|
||||||
|
|
@ -350,9 +379,75 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeWebcam()
|
changeAudioDevice(deviceId)
|
||||||
{
|
{
|
||||||
logger.debug('changeWebcam()');
|
logger.debug('changeAudioDevice() [deviceId: %s]', deviceId);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setAudioInProgress(true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
this._audioDevice.device = this._audioDevices.get(deviceId);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'changeAudioDevice() | new selected webcam [device:%o]',
|
||||||
|
this._audioDevice.device);
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
const { device } = this._audioDevice;
|
||||||
|
|
||||||
|
if (!device)
|
||||||
|
throw new Error('no audio devices');
|
||||||
|
|
||||||
|
logger.debug('changeAudioDevice() | calling getUserMedia()');
|
||||||
|
|
||||||
|
return navigator.mediaDevices.getUserMedia(
|
||||||
|
{
|
||||||
|
audio :
|
||||||
|
{
|
||||||
|
deviceId : { exact: device.deviceId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((stream) =>
|
||||||
|
{
|
||||||
|
const track = stream.getAudioTracks()[0];
|
||||||
|
|
||||||
|
return this._micProducer.replaceTrack(track)
|
||||||
|
.then((newTrack) =>
|
||||||
|
{
|
||||||
|
track.stop();
|
||||||
|
|
||||||
|
return newTrack;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((newTrack) =>
|
||||||
|
{
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setProducerTrack(this._micProducer.id, newTrack));
|
||||||
|
|
||||||
|
return this._updateAudioDevices();
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setAudioInProgress(false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('changeAudioDevice() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setAudioInProgress(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeWebcam(deviceId)
|
||||||
|
{
|
||||||
|
logger.debug('changeWebcam() [deviceId: %s]', deviceId);
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setWebcamInProgress(true));
|
stateActions.setWebcamInProgress(true));
|
||||||
|
|
@ -360,22 +455,7 @@ export default class RoomClient
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
return this._updateWebcams();
|
this._webcam.device = this._webcams.get(deviceId);
|
||||||
})
|
|
||||||
.then(() =>
|
|
||||||
{
|
|
||||||
const array = Array.from(this._webcams.keys());
|
|
||||||
const len = array.length;
|
|
||||||
const deviceId =
|
|
||||||
this._webcam.device ? this._webcam.device.deviceId : undefined;
|
|
||||||
let idx = array.indexOf(deviceId);
|
|
||||||
|
|
||||||
if (idx < len - 1)
|
|
||||||
idx++;
|
|
||||||
else
|
|
||||||
idx = 0;
|
|
||||||
|
|
||||||
this._webcam.device = this._webcams.get(array[idx]);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'changeWebcam() | new selected webcam [device:%o]',
|
'changeWebcam() | new selected webcam [device:%o]',
|
||||||
|
|
@ -386,7 +466,7 @@ export default class RoomClient
|
||||||
})
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
const { device, resolution } = this._webcam;
|
const { device } = this._webcam;
|
||||||
|
|
||||||
if (!device)
|
if (!device)
|
||||||
throw new Error('no webcam devices');
|
throw new Error('no webcam devices');
|
||||||
|
|
@ -398,7 +478,7 @@ export default class RoomClient
|
||||||
video :
|
video :
|
||||||
{
|
{
|
||||||
deviceId : { exact: device.deviceId },
|
deviceId : { exact: device.deviceId },
|
||||||
...VIDEO_CONSTRAINS[resolution]
|
...VIDEO_CONSTRAINS
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -419,6 +499,10 @@ export default class RoomClient
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
stateActions.setProducerTrack(this._webcamProducer.id, newTrack));
|
||||||
|
|
||||||
|
return this._updateWebcams();
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setWebcamInProgress(false));
|
stateActions.setWebcamInProgress(false));
|
||||||
})
|
})
|
||||||
|
|
@ -463,7 +547,7 @@ export default class RoomClient
|
||||||
})
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
const { device, resolution } = this._webcam;
|
const { device } = this._webcam;
|
||||||
|
|
||||||
logger.debug('changeWebcamResolution() | calling getUserMedia()');
|
logger.debug('changeWebcamResolution() | calling getUserMedia()');
|
||||||
|
|
||||||
|
|
@ -472,7 +556,7 @@ export default class RoomClient
|
||||||
video :
|
video :
|
||||||
{
|
{
|
||||||
deviceId : { exact: device.deviceId },
|
deviceId : { exact: device.deviceId },
|
||||||
...VIDEO_CONSTRAINS[resolution]
|
...VIDEO_CONSTRAINS
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -507,6 +591,222 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutePeerAudio(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('mutePeerAudio() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'mic')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.pause('mute-audio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('mutePeerAudio() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unmutePeerAudio(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('unmutePeerAudio() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'mic' || !consumer.supported)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('unmutePeerAudio() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerAudioInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePeerVideo(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('pausePeerVideo() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'webcam')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.pause('pause-video');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('pausePeerVideo() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resumePeerVideo(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('resumePeerVideo() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'webcam' || !consumer.supported)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('resumePeerVideo() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerVideoInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePeerScreen(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('pausePeerScreen() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'screen')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.pause('pause-screen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('pausePeerScreen() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resumePeerScreen(peerName)
|
||||||
|
{
|
||||||
|
logger.debug('resumePeerScreen() [peerName:"%s"]', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, true));
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
for (const peer of this._room.peers)
|
||||||
|
{
|
||||||
|
if (peer.name === peerName)
|
||||||
|
{
|
||||||
|
for (const consumer of peer.consumers)
|
||||||
|
{
|
||||||
|
if (consumer.appData.source !== 'screen' || !consumer.supported)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
consumer.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('resumePeerScreen() failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerScreenInProgress(peerName, false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
enableAudioOnly()
|
enableAudioOnly()
|
||||||
{
|
{
|
||||||
logger.debug('enableAudioOnly()');
|
logger.debug('enableAudioOnly()');
|
||||||
|
|
@ -587,6 +887,43 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendRaiseHandState(state)
|
||||||
|
{
|
||||||
|
logger.debug('sendRaiseHandState: ', state);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setMyRaiseHandStateInProgress(true));
|
||||||
|
|
||||||
|
return this._protoo.send('raisehand-message', { raiseHandState: state })
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setMyRaiseHandState(state));
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify(
|
||||||
|
{
|
||||||
|
text : 'raiseHand state changed'
|
||||||
|
}));
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setMyRaiseHandStateInProgress(false));
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
{
|
||||||
|
logger.error('sendRaiseHandState() | failed: %o', error);
|
||||||
|
|
||||||
|
this._dispatch(requestActions.notify(
|
||||||
|
{
|
||||||
|
type : 'error',
|
||||||
|
text : `Could not change raise hand state: ${error}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We need to refresh the component for it to render changed state
|
||||||
|
this._dispatch(stateActions.setMyRaiseHandState(!state));
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setMyRaiseHandStateInProgress(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
restartIce()
|
restartIce()
|
||||||
{
|
{
|
||||||
logger.debug('restartIce()');
|
logger.debug('restartIce()');
|
||||||
|
|
@ -714,6 +1051,48 @@ export default class RoomClient
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This means: server wants to change MY displayName
|
||||||
|
case 'auth':
|
||||||
|
{
|
||||||
|
logger.debug('got auth event from server', request.data);
|
||||||
|
accept();
|
||||||
|
|
||||||
|
if (request.data.verified == true)
|
||||||
|
{
|
||||||
|
this.changeDisplayName(request.data.name);
|
||||||
|
this._dispatch(requestActions.notify(
|
||||||
|
{
|
||||||
|
text : `Authenticated successfully: ${request.data}`
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._dispatch(requestActions.notify(
|
||||||
|
{
|
||||||
|
text : `Authentication failed: ${request.data}`
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
this.closeLoginWindow();
|
||||||
|
|
||||||
|
this._dispatch(stateActions.setLoginInProgress(false));
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'raisehand-message':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
const { peerName, raiseHandState } = request.data;
|
||||||
|
|
||||||
|
logger.debug('Got raiseHandState from "%s"', peerName);
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setPeerRaiseHandState(peerName, raiseHandState));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'chat-message-receive':
|
case 'chat-message-receive':
|
||||||
{
|
{
|
||||||
accept();
|
accept();
|
||||||
|
|
@ -926,6 +1305,12 @@ export default class RoomClient
|
||||||
let producer;
|
let producer;
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
logger.debug('_setMicProducer() | calling _updateAudioDevices()');
|
||||||
|
|
||||||
|
return this._updateAudioDevices();
|
||||||
|
})
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
logger.debug('_setMicProducer() | calling getUserMedia()');
|
logger.debug('_setMicProducer() | calling getUserMedia()');
|
||||||
|
|
@ -1078,6 +1463,14 @@ export default class RoomClient
|
||||||
this._dispatch(stateActions.removeProducer(producer.id));
|
this._dispatch(stateActions.removeProducer(producer.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
producer.on('trackended', (originator) =>
|
||||||
|
{
|
||||||
|
logger.debug(
|
||||||
|
'webcam Producer "trackended" event [originator:%s]', originator);
|
||||||
|
|
||||||
|
this.disableScreenSharing();
|
||||||
|
});
|
||||||
|
|
||||||
producer.on('pause', (originator) =>
|
producer.on('pause', (originator) =>
|
||||||
{
|
{
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
@ -1143,7 +1536,7 @@ export default class RoomClient
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() =>
|
.then(() =>
|
||||||
{
|
{
|
||||||
const { device, resolution } = this._webcam;
|
const { device } = this._webcam;
|
||||||
|
|
||||||
if (!device)
|
if (!device)
|
||||||
throw new Error('no webcam devices');
|
throw new Error('no webcam devices');
|
||||||
|
|
@ -1155,7 +1548,7 @@ export default class RoomClient
|
||||||
video :
|
video :
|
||||||
{
|
{
|
||||||
deviceId : { exact: device.deviceId },
|
deviceId : { exact: device.deviceId },
|
||||||
...VIDEO_CONSTRAINS[resolution]
|
...VIDEO_CONSTRAINS
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -1245,6 +1638,57 @@ export default class RoomClient
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateAudioDevices()
|
||||||
|
{
|
||||||
|
logger.debug('_updateAudioDevices()');
|
||||||
|
|
||||||
|
// Reset the list.
|
||||||
|
this._audioDevices = new Map();
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
logger.debug('_updateAudioDevices() | calling enumerateDevices()');
|
||||||
|
|
||||||
|
return navigator.mediaDevices.enumerateDevices();
|
||||||
|
})
|
||||||
|
.then((devices) =>
|
||||||
|
{
|
||||||
|
for (const device of devices)
|
||||||
|
{
|
||||||
|
if (device.kind !== 'audioinput')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
device.value = device.deviceId;
|
||||||
|
|
||||||
|
this._audioDevices.set(device.deviceId, device);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
{
|
||||||
|
const array = Array.from(this._audioDevices.values());
|
||||||
|
const len = array.length;
|
||||||
|
const currentAudioDeviceId =
|
||||||
|
this._audioDevice.device ? this._audioDevice.device.deviceId : undefined;
|
||||||
|
|
||||||
|
logger.debug('_updateAudioDevices() [audiodevices:%o]', array);
|
||||||
|
|
||||||
|
if (len === 0)
|
||||||
|
this._audioDevice.device = null;
|
||||||
|
else if (!this._audioDevices.has(currentAudioDeviceId))
|
||||||
|
this._audioDevice.device = array[0];
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setCanChangeAudioDevice(len >= 2));
|
||||||
|
if (len >= 1)
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setAudioDevices(this._audioDevices));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_updateWebcams()
|
_updateWebcams()
|
||||||
{
|
{
|
||||||
logger.debug('_updateWebcams()');
|
logger.debug('_updateWebcams()');
|
||||||
|
|
@ -1266,6 +1710,8 @@ export default class RoomClient
|
||||||
if (device.kind !== 'videoinput')
|
if (device.kind !== 'videoinput')
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
device.value = device.deviceId;
|
||||||
|
|
||||||
this._webcams.set(device.deviceId, device);
|
this._webcams.set(device.deviceId, device);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1285,6 +1731,12 @@ export default class RoomClient
|
||||||
|
|
||||||
this._dispatch(
|
this._dispatch(
|
||||||
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
stateActions.setCanChangeWebcam(this._webcams.size >= 2));
|
||||||
|
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setCanChangeWebcam(len >= 2));
|
||||||
|
if (len >= 1)
|
||||||
|
this._dispatch(
|
||||||
|
stateActions.setWebcamDevices(this._webcams));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,70 @@ class FirefoxScreenShare
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EdgeScreenShare
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this._stream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(options = {})
|
||||||
|
{
|
||||||
|
const constraints = this._toConstraints(options);
|
||||||
|
|
||||||
|
return navigator.getDisplayMedia(constraints)
|
||||||
|
.then((stream) =>
|
||||||
|
{
|
||||||
|
this._stream = stream;
|
||||||
|
|
||||||
|
return Promise.resolve(stream);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
{
|
||||||
|
if (this._stream instanceof MediaStream === false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._stream.getTracks().forEach((track) => track.stop());
|
||||||
|
this._stream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isScreenShareAvailable()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
needExtension()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_toConstraints()
|
||||||
|
{
|
||||||
|
const constraints = {
|
||||||
|
video : true
|
||||||
|
};
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultScreenShare
|
||||||
|
{
|
||||||
|
isScreenShareAvailable()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
needExtension()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class ScreenShare
|
export default class ScreenShare
|
||||||
{
|
{
|
||||||
static create()
|
static create()
|
||||||
|
|
@ -203,9 +267,13 @@ export default class ScreenShare
|
||||||
{
|
{
|
||||||
return new ChromeScreenShare();
|
return new ChromeScreenShare();
|
||||||
}
|
}
|
||||||
|
case 'edge':
|
||||||
|
{
|
||||||
|
return new EdgeScreenShare();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
return null;
|
return new DefaultScreenShare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as stateActions from '../../redux/stateActions';
|
||||||
|
import * as requestActions from '../../redux/requestActions';
|
||||||
|
import MessageList from './MessageList';
|
||||||
|
|
||||||
|
class Chat extends Component
|
||||||
|
{
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
senderPlaceHolder,
|
||||||
|
onSendMessage,
|
||||||
|
disabledInput,
|
||||||
|
autofocus,
|
||||||
|
displayName
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='Chat'>
|
||||||
|
<MessageList />
|
||||||
|
<form
|
||||||
|
data-component='Sender'
|
||||||
|
onSubmit={(e) => { onSendMessage(e, displayName); }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='new-message'
|
||||||
|
name='message'
|
||||||
|
placeholder={senderPlaceHolder}
|
||||||
|
disabled={disabledInput}
|
||||||
|
autoFocus={autofocus}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Chat.propTypes =
|
||||||
|
{
|
||||||
|
senderPlaceHolder : PropTypes.string,
|
||||||
|
onSendMessage : PropTypes.func,
|
||||||
|
disabledInput : PropTypes.bool,
|
||||||
|
autofocus : PropTypes.bool,
|
||||||
|
displayName : PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
Chat.defaultProps =
|
||||||
|
{
|
||||||
|
senderPlaceHolder : 'Type a message...',
|
||||||
|
autofocus : true,
|
||||||
|
displayName : null
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
disabledInput : state.chatbehavior.disabledInput,
|
||||||
|
displayName : state.me.displayName
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
onSendMessage : (event, displayName) =>
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
const userInput = event.target.message.value;
|
||||||
|
|
||||||
|
if (userInput)
|
||||||
|
{
|
||||||
|
dispatch(stateActions.addUserMessage(userInput));
|
||||||
|
dispatch(requestActions.sendChatMessage(userInput, displayName));
|
||||||
|
}
|
||||||
|
event.target.message.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChatContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Chat);
|
||||||
|
|
||||||
|
export default ChatContainer;
|
||||||
|
|
@ -7,6 +7,13 @@ import MessageList from './Chat/MessageList';
|
||||||
|
|
||||||
class ChatWidget extends Component
|
class ChatWidget extends Component
|
||||||
{
|
{
|
||||||
|
componentWillReceiveProps(nextProps)
|
||||||
|
{
|
||||||
|
if (nextProps.chatmessages.length !== this.props.chatmessages.length)
|
||||||
|
if (!this.props.showChat)
|
||||||
|
this.props.increaseBadge();
|
||||||
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
|
|
@ -68,15 +75,15 @@ ChatWidget.propTypes =
|
||||||
disabledInput : PropTypes.bool,
|
disabledInput : PropTypes.bool,
|
||||||
badge : PropTypes.number,
|
badge : PropTypes.number,
|
||||||
autofocus : PropTypes.bool,
|
autofocus : PropTypes.bool,
|
||||||
displayName : PropTypes.string
|
displayName : PropTypes.string,
|
||||||
|
chatmessages : PropTypes.arrayOf(PropTypes.object),
|
||||||
|
increaseBadge : PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
ChatWidget.defaultProps =
|
ChatWidget.defaultProps =
|
||||||
{
|
{
|
||||||
senderPlaceHolder : 'Type a message...',
|
senderPlaceHolder : 'Type a message...',
|
||||||
badge : 0,
|
autofocus : true
|
||||||
autofocus : true,
|
|
||||||
displayName : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -84,7 +91,9 @@ const mapStateToProps = (state) =>
|
||||||
return {
|
return {
|
||||||
showChat : state.chatbehavior.showChat,
|
showChat : state.chatbehavior.showChat,
|
||||||
disabledInput : state.chatbehavior.disabledInput,
|
disabledInput : state.chatbehavior.disabledInput,
|
||||||
displayName : state.me.displayName
|
displayName : state.me.displayName,
|
||||||
|
badge : state.chatbehavior.badge,
|
||||||
|
chatmessages : state.chatmessages
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,6 +115,10 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
dispatch(requestActions.sendChatMessage(userInput, displayName));
|
dispatch(requestActions.sendChatMessage(userInput, displayName));
|
||||||
}
|
}
|
||||||
event.target.message.value = '';
|
event.target.message.value = '';
|
||||||
|
},
|
||||||
|
increaseBadge : () =>
|
||||||
|
{
|
||||||
|
dispatch(stateActions.increaseBadge());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import * as appPropTypes from './appPropTypes';
|
||||||
|
import * as stateActions from '../redux/stateActions';
|
||||||
|
import FullView from './FullView';
|
||||||
|
|
||||||
|
const FullScreenView = (props) =>
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
advancedMode,
|
||||||
|
consumer,
|
||||||
|
toggleConsumerFullscreen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!consumer)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const consumerVisible = (
|
||||||
|
Boolean(consumer) &&
|
||||||
|
!consumer.locallyPaused &&
|
||||||
|
!consumer.remotelyPaused
|
||||||
|
);
|
||||||
|
|
||||||
|
let consumerProfile;
|
||||||
|
|
||||||
|
if (consumer)
|
||||||
|
consumerProfile = consumer.profile;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='FullScreenView'>
|
||||||
|
{consumerVisible && !consumer.supported ?
|
||||||
|
<div className='incompatible-video'>
|
||||||
|
<p>incompatible video</p>
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className='controls'>
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'fullscreen')}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleConsumerFullscreen(consumer);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FullView
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
videoTrack={consumer ? consumer.track : null}
|
||||||
|
videoVisible={consumerVisible}
|
||||||
|
videoProfile={consumerProfile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FullScreenView.propTypes =
|
||||||
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
consumer : appPropTypes.Consumer,
|
||||||
|
toggleConsumerFullscreen : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
consumer : state.consumers[state.room.fullScreenConsumer]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
toggleConsumerFullscreen : (consumer) =>
|
||||||
|
{
|
||||||
|
if (consumer)
|
||||||
|
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FullScreenViewContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FullScreenView);
|
||||||
|
|
||||||
|
export default FullScreenViewContainer;
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Spinner from 'react-spinner';
|
||||||
|
|
||||||
|
export default class FullView extends React.Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Latest received video track.
|
||||||
|
// @type {MediaStreamTrack}
|
||||||
|
this._videoTrack = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
videoVisible,
|
||||||
|
videoProfile
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='FullView'>
|
||||||
|
<video
|
||||||
|
ref='video'
|
||||||
|
className={classnames({
|
||||||
|
hidden : !videoVisible,
|
||||||
|
loading : videoProfile === 'none'
|
||||||
|
})}
|
||||||
|
autoPlay
|
||||||
|
muted={Boolean(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{videoProfile === 'none' ?
|
||||||
|
<div className='spinner-container'>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
const { videoTrack } = this.props;
|
||||||
|
|
||||||
|
this._setTracks(videoTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps)
|
||||||
|
{
|
||||||
|
const { videoTrack } = nextProps;
|
||||||
|
|
||||||
|
this._setTracks(videoTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setTracks(videoTrack)
|
||||||
|
{
|
||||||
|
if (this._videoTrack === videoTrack)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._videoTrack = videoTrack;
|
||||||
|
|
||||||
|
const { video } = this.refs;
|
||||||
|
|
||||||
|
if (videoTrack)
|
||||||
|
{
|
||||||
|
const stream = new MediaStream;
|
||||||
|
|
||||||
|
if (videoTrack)
|
||||||
|
stream.addTrack(videoTrack);
|
||||||
|
|
||||||
|
video.srcObject = stream;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FullView.propTypes =
|
||||||
|
{
|
||||||
|
videoTrack : PropTypes.any,
|
||||||
|
videoVisible : PropTypes.bool,
|
||||||
|
videoProfile : PropTypes.string
|
||||||
|
};
|
||||||
|
|
@ -7,6 +7,7 @@ import { getDeviceInfo } from 'mediasoup-client';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import * as requestActions from '../redux/requestActions';
|
import * as requestActions from '../redux/requestActions';
|
||||||
import PeerView from './PeerView';
|
import PeerView from './PeerView';
|
||||||
|
import ScreenView from './ScreenView';
|
||||||
|
|
||||||
class Me extends React.Component
|
class Me extends React.Component
|
||||||
{
|
{
|
||||||
|
|
@ -29,14 +30,15 @@ class Me extends React.Component
|
||||||
const {
|
const {
|
||||||
connected,
|
connected,
|
||||||
me,
|
me,
|
||||||
|
advancedMode,
|
||||||
micProducer,
|
micProducer,
|
||||||
webcamProducer,
|
webcamProducer,
|
||||||
|
screenProducer,
|
||||||
onChangeDisplayName,
|
onChangeDisplayName,
|
||||||
onMuteMic,
|
onMuteMic,
|
||||||
onUnmuteMic,
|
onUnmuteMic,
|
||||||
onEnableWebcam,
|
onEnableWebcam,
|
||||||
onDisableWebcam,
|
onDisableWebcam
|
||||||
onChangeWebcam
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let micState;
|
let micState;
|
||||||
|
|
@ -59,19 +61,18 @@ class Me extends React.Component
|
||||||
else
|
else
|
||||||
webcamState = 'off';
|
webcamState = 'off';
|
||||||
|
|
||||||
let changeWebcamState;
|
|
||||||
|
|
||||||
if (Boolean(webcamProducer) && me.canChangeWebcam)
|
|
||||||
changeWebcamState = 'on';
|
|
||||||
else
|
|
||||||
changeWebcamState = 'unsupported';
|
|
||||||
|
|
||||||
const videoVisible = (
|
const videoVisible = (
|
||||||
Boolean(webcamProducer) &&
|
Boolean(webcamProducer) &&
|
||||||
!webcamProducer.locallyPaused &&
|
!webcamProducer.locallyPaused &&
|
||||||
!webcamProducer.remotelyPaused
|
!webcamProducer.remotelyPaused
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const screenVisible = (
|
||||||
|
Boolean(screenProducer) &&
|
||||||
|
!screenProducer.locallyPaused &&
|
||||||
|
!screenProducer.remotelyPaused
|
||||||
|
);
|
||||||
|
|
||||||
let tip;
|
let tip;
|
||||||
|
|
||||||
if (!me.displayNameSet)
|
if (!me.displayNameSet)
|
||||||
|
|
@ -85,10 +86,13 @@ class Me extends React.Component
|
||||||
data-tip-disable={!tip}
|
data-tip-disable={!tip}
|
||||||
data-type='dark'
|
data-type='dark'
|
||||||
>
|
>
|
||||||
|
<div className={classnames('view-container', 'webcam')}>
|
||||||
{connected ?
|
{connected ?
|
||||||
<div className='controls'>
|
<div className='controls'>
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'mic', micState)}
|
className={classnames('button', 'mic', micState, {
|
||||||
|
disabled : me.audioInProgress
|
||||||
|
})}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
{
|
{
|
||||||
micState === 'on' ? onMuteMic() : onUnmuteMic();
|
micState === 'on' ? onMuteMic() : onUnmuteMic();
|
||||||
|
|
@ -104,19 +108,13 @@ class Me extends React.Component
|
||||||
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam();
|
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'change-webcam', changeWebcamState, {
|
|
||||||
disabled : me.webcamInProgress
|
|
||||||
})}
|
|
||||||
onClick={() => onChangeWebcam()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
<PeerView
|
<PeerView
|
||||||
isMe
|
isMe
|
||||||
|
advancedMode={advancedMode}
|
||||||
peer={me}
|
peer={me}
|
||||||
audioTrack={micProducer ? micProducer.track : null}
|
audioTrack={micProducer ? micProducer.track : null}
|
||||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||||
|
|
@ -125,6 +123,20 @@ class Me extends React.Component
|
||||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||||
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
|
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{screenProducer ?
|
||||||
|
<div className={classnames('view-container', 'screen')}>
|
||||||
|
<ScreenView
|
||||||
|
isMe
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
screenTrack={screenProducer ? screenProducer.track : null}
|
||||||
|
screenVisible={screenVisible}
|
||||||
|
screenCodec={screenProducer ? screenProducer.codec : null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
{this._tooltip ?
|
{this._tooltip ?
|
||||||
<ReactTooltip
|
<ReactTooltip
|
||||||
|
|
@ -172,15 +184,16 @@ class Me extends React.Component
|
||||||
Me.propTypes =
|
Me.propTypes =
|
||||||
{
|
{
|
||||||
connected : PropTypes.bool.isRequired,
|
connected : PropTypes.bool.isRequired,
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
me : appPropTypes.Me.isRequired,
|
me : appPropTypes.Me.isRequired,
|
||||||
micProducer : appPropTypes.Producer,
|
micProducer : appPropTypes.Producer,
|
||||||
webcamProducer : appPropTypes.Producer,
|
webcamProducer : appPropTypes.Producer,
|
||||||
|
screenProducer : appPropTypes.Producer,
|
||||||
onChangeDisplayName : PropTypes.func.isRequired,
|
onChangeDisplayName : PropTypes.func.isRequired,
|
||||||
onMuteMic : PropTypes.func.isRequired,
|
onMuteMic : PropTypes.func.isRequired,
|
||||||
onUnmuteMic : PropTypes.func.isRequired,
|
onUnmuteMic : PropTypes.func.isRequired,
|
||||||
onEnableWebcam : PropTypes.func.isRequired,
|
onEnableWebcam : PropTypes.func.isRequired,
|
||||||
onDisableWebcam : PropTypes.func.isRequired,
|
onDisableWebcam : PropTypes.func.isRequired
|
||||||
onChangeWebcam : PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -190,12 +203,15 @@ const mapStateToProps = (state) =>
|
||||||
producersArray.find((producer) => producer.source === 'mic');
|
producersArray.find((producer) => producer.source === 'mic');
|
||||||
const webcamProducer =
|
const webcamProducer =
|
||||||
producersArray.find((producer) => producer.source === 'webcam');
|
producersArray.find((producer) => producer.source === 'webcam');
|
||||||
|
const screenProducer =
|
||||||
|
producersArray.find((producer) => producer.source === 'screen');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connected : state.room.state === 'connected',
|
connected : state.room.state === 'connected',
|
||||||
me : state.me,
|
me : state.me,
|
||||||
micProducer : micProducer,
|
micProducer : micProducer,
|
||||||
webcamProducer : webcamProducer
|
webcamProducer : webcamProducer,
|
||||||
|
screenProducer : screenProducer
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -209,8 +225,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
onMuteMic : () => dispatch(requestActions.muteMic()),
|
onMuteMic : () => dispatch(requestActions.muteMic()),
|
||||||
onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
|
onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
|
||||||
onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
|
onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
|
||||||
onDisableWebcam : () => dispatch(requestActions.disableWebcam()),
|
onDisableWebcam : () => dispatch(requestActions.disableWebcam())
|
||||||
onChangeWebcam : () => dispatch(requestActions.changeWebcam())
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import * as appPropTypes from '../appPropTypes';
|
||||||
|
import * as requestActions from '../../redux/requestActions';
|
||||||
|
|
||||||
|
const ListPeer = (props) =>
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
peer,
|
||||||
|
micConsumer,
|
||||||
|
webcamConsumer,
|
||||||
|
screenConsumer,
|
||||||
|
onMuteMic,
|
||||||
|
onUnmuteMic,
|
||||||
|
onDisableWebcam,
|
||||||
|
onEnableWebcam,
|
||||||
|
onDisableScreen,
|
||||||
|
onEnableScreen
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const micEnabled = (
|
||||||
|
Boolean(micConsumer) &&
|
||||||
|
!micConsumer.locallyPaused &&
|
||||||
|
!micConsumer.remotelyPaused
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoVisible = (
|
||||||
|
Boolean(webcamConsumer) &&
|
||||||
|
!webcamConsumer.locallyPaused &&
|
||||||
|
!webcamConsumer.remotelyPaused
|
||||||
|
);
|
||||||
|
|
||||||
|
const screenVisible = (
|
||||||
|
Boolean(screenConsumer) &&
|
||||||
|
!screenConsumer.locallyPaused &&
|
||||||
|
!screenConsumer.remotelyPaused
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='ListPeer'>
|
||||||
|
<img className='avatar' />
|
||||||
|
<div className='peer-info'>
|
||||||
|
{peer.displayName}
|
||||||
|
</div>
|
||||||
|
<div className='controls'>
|
||||||
|
{ screenConsumer ?
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'screen', {
|
||||||
|
on : screenVisible,
|
||||||
|
off : !screenVisible,
|
||||||
|
disabled : peer.peerScreenInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
screenVisible ?
|
||||||
|
onDisableScreen(peer.name) : onEnableScreen(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'mic', {
|
||||||
|
on : micEnabled,
|
||||||
|
off : !micEnabled,
|
||||||
|
disabled : peer.peerAudioInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'webcam', {
|
||||||
|
on : videoVisible,
|
||||||
|
off : !videoVisible,
|
||||||
|
disabled : peer.peerVideoInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
videoVisible ?
|
||||||
|
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ListPeer.propTypes =
|
||||||
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
peer : appPropTypes.Peer.isRequired,
|
||||||
|
micConsumer : appPropTypes.Consumer,
|
||||||
|
webcamConsumer : appPropTypes.Consumer,
|
||||||
|
screenConsumer : appPropTypes.Consumer,
|
||||||
|
onMuteMic : PropTypes.func.isRequired,
|
||||||
|
onUnmuteMic : PropTypes.func.isRequired,
|
||||||
|
onEnableWebcam : PropTypes.func.isRequired,
|
||||||
|
onDisableWebcam : PropTypes.func.isRequired,
|
||||||
|
onEnableScreen : PropTypes.func.isRequired,
|
||||||
|
onDisableScreen : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { name }) =>
|
||||||
|
{
|
||||||
|
const peer = state.peers[name];
|
||||||
|
const consumersArray = peer.consumers
|
||||||
|
.map((consumerId) => state.consumers[consumerId]);
|
||||||
|
const micConsumer =
|
||||||
|
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||||
|
const webcamConsumer =
|
||||||
|
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||||
|
const screenConsumer =
|
||||||
|
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||||
|
|
||||||
|
return {
|
||||||
|
peer,
|
||||||
|
micConsumer,
|
||||||
|
webcamConsumer,
|
||||||
|
screenConsumer
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
onMuteMic : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.mutePeerAudio(peerName));
|
||||||
|
},
|
||||||
|
onUnmuteMic : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.unmutePeerAudio(peerName));
|
||||||
|
},
|
||||||
|
onEnableWebcam : (peerName) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
dispatch(requestActions.resumePeerVideo(peerName));
|
||||||
|
},
|
||||||
|
onDisableWebcam : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.pausePeerVideo(peerName));
|
||||||
|
},
|
||||||
|
onEnableScreen : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.resumePeerScreen(peerName));
|
||||||
|
},
|
||||||
|
onDisableScreen : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.pausePeerScreen(peerName));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListPeerContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ListPeer);
|
||||||
|
|
||||||
|
export default ListPeerContainer;
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as appPropTypes from '../appPropTypes';
|
||||||
|
import * as requestActions from '../../redux/requestActions';
|
||||||
|
import * as stateActions from '../../redux/stateActions';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ListPeer from './ListPeer';
|
||||||
|
|
||||||
|
class ParticipantList extends React.Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
advancedMode,
|
||||||
|
peers
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='ParticipantList'>
|
||||||
|
<ul className='list'>
|
||||||
|
{
|
||||||
|
peers.map((peer) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<li key={peer.name} className='list-item'>
|
||||||
|
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParticipantList.propTypes =
|
||||||
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
const peersArray = Object.values(state.peers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
peers : peersArray
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
handleChangeWebcam : (device) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.changeWebcam(device.value));
|
||||||
|
},
|
||||||
|
handleChangeAudioDevice : (device) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.changeAudioDevice(device.value));
|
||||||
|
},
|
||||||
|
onToggleAdvancedMode : () =>
|
||||||
|
{
|
||||||
|
dispatch(stateActions.toggleAdvancedMode());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParticipantListContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ParticipantList);
|
||||||
|
|
||||||
|
export default ParticipantListContainer;
|
||||||
|
|
@ -1,15 +1,29 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
|
import * as requestActions from '../redux/requestActions';
|
||||||
|
import * as stateActions from '../redux/stateActions';
|
||||||
import PeerView from './PeerView';
|
import PeerView from './PeerView';
|
||||||
|
import ScreenView from './ScreenView';
|
||||||
|
|
||||||
const Peer = (props) =>
|
const Peer = (props) =>
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
|
advancedMode,
|
||||||
peer,
|
peer,
|
||||||
micConsumer,
|
micConsumer,
|
||||||
webcamConsumer,
|
webcamConsumer,
|
||||||
screenConsumer
|
screenConsumer,
|
||||||
|
onMuteMic,
|
||||||
|
onUnmuteMic,
|
||||||
|
onDisableWebcam,
|
||||||
|
onEnableWebcam,
|
||||||
|
onDisableScreen,
|
||||||
|
onEnableScreen,
|
||||||
|
toggleConsumerFullscreen,
|
||||||
|
style
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const micEnabled = (
|
const micEnabled = (
|
||||||
|
|
@ -41,18 +55,12 @@ const Peer = (props) =>
|
||||||
screenProfile = screenConsumer.profile;
|
screenProfile = screenConsumer.profile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='Peer'>
|
<div
|
||||||
<div className='indicators'>
|
data-component='Peer'
|
||||||
{!micEnabled ?
|
className={classnames({
|
||||||
<div className='icon mic-off' />
|
screen : screenConsumer
|
||||||
:null
|
})}
|
||||||
}
|
>
|
||||||
{!videoVisible ?
|
|
||||||
<div className='icon webcam-off' />
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{videoVisible && !webcamConsumer.supported ?
|
{videoVisible && !webcamConsumer.supported ?
|
||||||
<div className='incompatible-video'>
|
<div className='incompatible-video'>
|
||||||
<p>incompatible video</p>
|
<p>incompatible video</p>
|
||||||
|
|
@ -60,29 +68,112 @@ const Peer = (props) =>
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||||
|
<div className='controls'>
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'mic', {
|
||||||
|
on : micEnabled,
|
||||||
|
off : !micEnabled,
|
||||||
|
disabled : peer.peerAudioInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'webcam', {
|
||||||
|
on : videoVisible,
|
||||||
|
off : !videoVisible,
|
||||||
|
disabled : peer.peerVideoInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
videoVisible ?
|
||||||
|
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'fullscreen')}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleConsumerFullscreen(webcamConsumer);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<PeerView
|
<PeerView
|
||||||
|
advancedMode={advancedMode}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
audioTrack={micConsumer ? micConsumer.track : null}
|
audioTrack={micConsumer ? micConsumer.track : null}
|
||||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||||
screenTrack={screenConsumer ? screenConsumer.track : null}
|
|
||||||
videoVisible={videoVisible}
|
videoVisible={videoVisible}
|
||||||
videoProfile={videoProfile}
|
videoProfile={videoProfile}
|
||||||
screenVisible={screenVisible}
|
|
||||||
screenProfile={screenProfile}
|
|
||||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{screenConsumer ?
|
||||||
|
<div className={classnames('view-container', 'screen')} style={style}>
|
||||||
|
<div className='controls'>
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'screen', {
|
||||||
|
on : screenVisible,
|
||||||
|
off : !screenVisible,
|
||||||
|
disabled : peer.peerScreenInProgress
|
||||||
|
})}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
screenVisible ?
|
||||||
|
onDisableScreen(peer.name) : onEnableScreen(peer.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'fullscreen')}
|
||||||
|
onClick={(e) =>
|
||||||
|
{
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleConsumerFullscreen(screenConsumer);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScreenView
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
screenTrack={screenConsumer ? screenConsumer.track : null}
|
||||||
|
screenVisible={screenVisible}
|
||||||
|
screenProfile={screenProfile}
|
||||||
screenCodec={screenConsumer ? screenConsumer.codec : null}
|
screenCodec={screenConsumer ? screenConsumer.codec : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Peer.propTypes =
|
Peer.propTypes =
|
||||||
{
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
peer : appPropTypes.Peer.isRequired,
|
peer : appPropTypes.Peer.isRequired,
|
||||||
micConsumer : appPropTypes.Consumer,
|
micConsumer : appPropTypes.Consumer,
|
||||||
webcamConsumer : appPropTypes.Consumer,
|
webcamConsumer : appPropTypes.Consumer,
|
||||||
screenConsumer : appPropTypes.Consumer
|
screenConsumer : appPropTypes.Consumer,
|
||||||
|
onMuteMic : PropTypes.func.isRequired,
|
||||||
|
onUnmuteMic : PropTypes.func.isRequired,
|
||||||
|
onEnableWebcam : PropTypes.func.isRequired,
|
||||||
|
onDisableWebcam : PropTypes.func.isRequired,
|
||||||
|
streamDimensions : PropTypes.object,
|
||||||
|
style : PropTypes.object,
|
||||||
|
onEnableScreen : PropTypes.func.isRequired,
|
||||||
|
onDisableScreen : PropTypes.func.isRequired,
|
||||||
|
toggleConsumerFullscreen : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state, { name }) =>
|
const mapStateToProps = (state, { name }) =>
|
||||||
|
|
@ -105,6 +196,45 @@ const mapStateToProps = (state, { name }) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const PeerContainer = connect(mapStateToProps)(Peer);
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
onMuteMic : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.mutePeerAudio(peerName));
|
||||||
|
},
|
||||||
|
onUnmuteMic : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.unmutePeerAudio(peerName));
|
||||||
|
},
|
||||||
|
onEnableWebcam : (peerName) =>
|
||||||
|
{
|
||||||
|
|
||||||
|
dispatch(requestActions.resumePeerVideo(peerName));
|
||||||
|
},
|
||||||
|
onDisableWebcam : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.pausePeerVideo(peerName));
|
||||||
|
},
|
||||||
|
onEnableScreen : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.resumePeerScreen(peerName));
|
||||||
|
},
|
||||||
|
onDisableScreen : (peerName) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.pausePeerScreen(peerName));
|
||||||
|
},
|
||||||
|
toggleConsumerFullscreen : (consumer) =>
|
||||||
|
{
|
||||||
|
if (consumer)
|
||||||
|
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const PeerContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Peer);
|
||||||
|
|
||||||
export default PeerContainer;
|
export default PeerContainer;
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ export default class PeerView extends React.Component
|
||||||
{
|
{
|
||||||
volume : 0, // Integer from 0 to 10.,
|
volume : 0, // Integer from 0 to 10.,
|
||||||
videoWidth : null,
|
videoWidth : null,
|
||||||
videoHeight : null,
|
videoHeight : null
|
||||||
screenWidth : null,
|
|
||||||
screenHeight : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Latest received video track.
|
// Latest received video track.
|
||||||
|
|
@ -29,10 +27,6 @@ export default class PeerView extends React.Component
|
||||||
// @type {MediaStreamTrack}
|
// @type {MediaStreamTrack}
|
||||||
this._videoTrack = null;
|
this._videoTrack = null;
|
||||||
|
|
||||||
// Latest received screen track.
|
|
||||||
// @type {MediaStreamTrack}
|
|
||||||
this._screenTrack = null;
|
|
||||||
|
|
||||||
// Hark instance.
|
// Hark instance.
|
||||||
// @type {Object}
|
// @type {Object}
|
||||||
this._hark = null;
|
this._hark = null;
|
||||||
|
|
@ -46,51 +40,31 @@ export default class PeerView extends React.Component
|
||||||
const {
|
const {
|
||||||
isMe,
|
isMe,
|
||||||
peer,
|
peer,
|
||||||
|
advancedMode,
|
||||||
videoVisible,
|
videoVisible,
|
||||||
videoProfile,
|
videoProfile,
|
||||||
screenVisible,
|
|
||||||
screenProfile,
|
|
||||||
audioCodec,
|
audioCodec,
|
||||||
videoCodec,
|
videoCodec,
|
||||||
screenCodec,
|
|
||||||
onChangeDisplayName
|
onChangeDisplayName
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
volume,
|
volume,
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight,
|
videoHeight
|
||||||
screenWidth,
|
|
||||||
screenHeight
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='PeerView'>
|
<div data-component='PeerView'>
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
{advancedMode ?
|
||||||
<div className={classnames('media', { 'is-me': isMe })}>
|
<div className={classnames('media', { 'is-me': isMe })}>
|
||||||
{screenVisible ?
|
|
||||||
<div className='box'>
|
<div className='box'>
|
||||||
{audioCodec ?
|
{audioCodec ?
|
||||||
<p className='codec'>{audioCodec}</p>
|
<p className='codec'>{audioCodec}</p>
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
|
|
||||||
{screenCodec ?
|
|
||||||
<p className='codec'>{screenCodec} {screenProfile}</p>
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
|
|
||||||
{(screenVisible && screenWidth !== null) ?
|
|
||||||
<p className='resolution'>{screenWidth}x{screenHeight}</p>
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
:<div className='box'>
|
|
||||||
{audioCodec ?
|
|
||||||
<p className='codec'>{audioCodec}</p>
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
|
|
||||||
{videoCodec ?
|
{videoCodec ?
|
||||||
<p className='codec'>{videoCodec} {videoProfile}</p>
|
<p className='codec'>{videoCodec} {videoProfile}</p>
|
||||||
:null
|
:null
|
||||||
|
|
@ -101,8 +75,9 @@ export default class PeerView extends React.Component
|
||||||
:null
|
:null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
<div className={classnames('peer', { 'is-me': isMe })}>
|
<div className={classnames('peer', { 'is-me': isMe })}>
|
||||||
{isMe ?
|
{isMe ?
|
||||||
|
|
@ -126,6 +101,7 @@ export default class PeerView extends React.Component
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{advancedMode ?
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<span
|
<span
|
||||||
className={classnames('device-icon', peer.device.flag)}
|
className={classnames('device-icon', peer.device.flag)}
|
||||||
|
|
@ -134,24 +110,13 @@ export default class PeerView extends React.Component
|
||||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<video
|
<video
|
||||||
ref='video'
|
ref='video'
|
||||||
className={classnames({
|
|
||||||
hidden : !videoVisible && !screenVisible,
|
|
||||||
'is-me' : isMe,
|
|
||||||
loading : videoProfile === 'none' && screenProfile === 'none'
|
|
||||||
})}
|
|
||||||
autoPlay
|
|
||||||
muted={isMe}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{screenVisible ?
|
|
||||||
<div className='minivideo'>
|
|
||||||
<video
|
|
||||||
ref='minivideo'
|
|
||||||
className={classnames({
|
className={classnames({
|
||||||
hidden : !videoVisible,
|
hidden : !videoVisible,
|
||||||
'is-me' : isMe,
|
'is-me' : isMe,
|
||||||
|
|
@ -160,15 +125,12 @@ export default class PeerView extends React.Component
|
||||||
autoPlay
|
autoPlay
|
||||||
muted={isMe}
|
muted={isMe}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
:null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='volume-container'>
|
<div className='volume-container'>
|
||||||
<div className={classnames('bar', `level${volume}`)} />
|
<div className={classnames('bar', `level${volume}`)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{videoProfile === 'none' && screenProfile === 'none' ?
|
{videoProfile === 'none' ?
|
||||||
<div className='spinner-container'>
|
<div className='spinner-container'>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -180,9 +142,9 @@ export default class PeerView extends React.Component
|
||||||
|
|
||||||
componentDidMount()
|
componentDidMount()
|
||||||
{
|
{
|
||||||
const { audioTrack, videoTrack, screenTrack } = this.props;
|
const { audioTrack, videoTrack } = this.props;
|
||||||
|
|
||||||
this._setTracks(audioTrack, videoTrack, screenTrack);
|
this._setTracks(audioTrack, videoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount()
|
componentWillUnmount()
|
||||||
|
|
@ -195,21 +157,18 @@ export default class PeerView extends React.Component
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps)
|
componentWillReceiveProps(nextProps)
|
||||||
{
|
{
|
||||||
const { audioTrack, videoTrack, screenTrack } = nextProps;
|
const { audioTrack, videoTrack } = nextProps;
|
||||||
|
|
||||||
this._setTracks(audioTrack, videoTrack, screenTrack);
|
this._setTracks(audioTrack, videoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setTracks(audioTrack, videoTrack, screenTrack)
|
_setTracks(audioTrack, videoTrack)
|
||||||
{
|
{
|
||||||
if (this._audioTrack === audioTrack &&
|
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||||
this._videoTrack === videoTrack &&
|
|
||||||
this._screenTrack === screenTrack)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this._audioTrack = audioTrack;
|
this._audioTrack = audioTrack;
|
||||||
this._videoTrack = videoTrack;
|
this._videoTrack = videoTrack;
|
||||||
this._screenTrack = screenTrack;
|
|
||||||
|
|
||||||
if (this._hark)
|
if (this._hark)
|
||||||
this._hark.stop();
|
this._hark.stop();
|
||||||
|
|
@ -217,9 +176,9 @@ export default class PeerView extends React.Component
|
||||||
clearInterval(this._videoResolutionTimer);
|
clearInterval(this._videoResolutionTimer);
|
||||||
this._hideVideoResolution();
|
this._hideVideoResolution();
|
||||||
|
|
||||||
const { video, minivideo } = this.refs;
|
const { video } = this.refs;
|
||||||
|
|
||||||
if (audioTrack || videoTrack || screenTrack)
|
if (audioTrack || videoTrack)
|
||||||
{
|
{
|
||||||
const stream = new MediaStream;
|
const stream = new MediaStream;
|
||||||
|
|
||||||
|
|
@ -229,19 +188,7 @@ export default class PeerView extends React.Component
|
||||||
if (videoTrack)
|
if (videoTrack)
|
||||||
stream.addTrack(videoTrack);
|
stream.addTrack(videoTrack);
|
||||||
|
|
||||||
if (screenTrack)
|
|
||||||
{
|
|
||||||
const screenStream = new MediaStream;
|
|
||||||
|
|
||||||
screenStream.addTrack(screenTrack);
|
|
||||||
|
|
||||||
video.srcObject = screenStream;
|
|
||||||
minivideo.srcObject = stream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
}
|
|
||||||
|
|
||||||
if (audioTrack)
|
if (audioTrack)
|
||||||
this._runHark(stream);
|
this._runHark(stream);
|
||||||
|
|
@ -310,15 +257,12 @@ PeerView.propTypes =
|
||||||
isMe : PropTypes.bool,
|
isMe : PropTypes.bool,
|
||||||
peer : PropTypes.oneOfType(
|
peer : PropTypes.oneOfType(
|
||||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
audioTrack : PropTypes.any,
|
audioTrack : PropTypes.any,
|
||||||
videoTrack : PropTypes.any,
|
videoTrack : PropTypes.any,
|
||||||
screenTrack : PropTypes.any,
|
|
||||||
videoVisible : PropTypes.bool.isRequired,
|
videoVisible : PropTypes.bool.isRequired,
|
||||||
videoProfile : PropTypes.string,
|
videoProfile : PropTypes.string,
|
||||||
screenVisible : PropTypes.bool.isRequired,
|
|
||||||
screenProfile : PropTypes.string,
|
|
||||||
audioCodec : PropTypes.string,
|
audioCodec : PropTypes.string,
|
||||||
videoCodec : PropTypes.string,
|
videoCodec : PropTypes.string,
|
||||||
screenCodec : PropTypes.string,
|
|
||||||
onChangeDisplayName : PropTypes.func
|
onChangeDisplayName : PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,29 @@ import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import * as appPropTypes from './appPropTypes';
|
import * as appPropTypes from './appPropTypes';
|
||||||
import * as stateActions from '../redux/stateActions';
|
|
||||||
import { Appear } from './transitions';
|
import { Appear } from './transitions';
|
||||||
import Peer from './Peer';
|
import Peer from './Peer';
|
||||||
|
|
||||||
class Peers extends React.Component
|
class Peers extends React.Component
|
||||||
{
|
{
|
||||||
constructor()
|
constructor(props)
|
||||||
{
|
{
|
||||||
super();
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
ratio : 4 / 3
|
peerWidth : 400,
|
||||||
|
peerHeight : 300,
|
||||||
|
ratio : 1.334
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
updateDimensions()
|
|
||||||
|
resizeUpdate()
|
||||||
{
|
{
|
||||||
const n = this.props.peers.length;
|
this.updateDimensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDimensions(props = this.props)
|
||||||
|
{
|
||||||
|
const n = props.videoStreams ? props.videoStreams : 0;
|
||||||
|
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -47,39 +53,45 @@ class Peers extends React.Component
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Math.ceil(this.props.peerWidth) !== Math.ceil(0.9 * x))
|
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
|
||||||
{
|
{
|
||||||
this.props.onComponentResize(0.9 * x, 0.9 * y);
|
this.setState({
|
||||||
|
peerWidth : 0.9 * x,
|
||||||
|
peerHeight : 0.9 * y
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount()
|
componentDidMount()
|
||||||
{
|
{
|
||||||
window.addEventListener('resize', this.updateDimensions.bind(this));
|
window.addEventListener('resize', this.resizeUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount()
|
componentWillUnmount()
|
||||||
{
|
{
|
||||||
window.removeEventListener('resize', this.updateDimensions.bind(this));
|
window.removeEventListener('resize', this.resizeUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps)
|
||||||
|
{
|
||||||
|
this.updateDimensions(nextProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
{
|
{
|
||||||
const {
|
const {
|
||||||
|
advancedMode,
|
||||||
activeSpeakerName,
|
activeSpeakerName,
|
||||||
peers,
|
peers,
|
||||||
peerWidth,
|
toolAreaOpen
|
||||||
peerHeight
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const style =
|
const style =
|
||||||
{
|
{
|
||||||
'width' : peerWidth,
|
'width' : this.state.peerWidth,
|
||||||
'height' : peerHeight
|
'height' : this.state.peerHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateDimensions();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component='Peers' ref='peers'>
|
<div data-component='Peers' ref='peers'>
|
||||||
{
|
{
|
||||||
|
|
@ -90,9 +102,14 @@ class Peers extends React.Component
|
||||||
<div
|
<div
|
||||||
className={classnames('peer-container', {
|
className={classnames('peer-container', {
|
||||||
'active-speaker' : peer.name === activeSpeakerName
|
'active-speaker' : peer.name === activeSpeakerName
|
||||||
})} style={style}
|
})}
|
||||||
>
|
>
|
||||||
<Peer name={peer.name} />
|
<Peer
|
||||||
|
advancedMode={advancedMode}
|
||||||
|
name={peer.name}
|
||||||
|
style={style}
|
||||||
|
toolAreaOpen={toolAreaOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Appear>
|
</Appear>
|
||||||
);
|
);
|
||||||
|
|
@ -105,40 +122,33 @@ class Peers extends React.Component
|
||||||
|
|
||||||
Peers.propTypes =
|
Peers.propTypes =
|
||||||
{
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||||
|
videoStreams : PropTypes.any,
|
||||||
activeSpeakerName : PropTypes.string,
|
activeSpeakerName : PropTypes.string,
|
||||||
peerHeight : PropTypes.number,
|
toolAreaOpen : PropTypes.bool
|
||||||
peerWidth : PropTypes.number,
|
|
||||||
onComponentResize : PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) =>
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
onComponentResize : (peerWidth, peerHeight) =>
|
|
||||||
{
|
|
||||||
dispatch(stateActions.onComponentResize(peerWidth, peerHeight));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
{
|
{
|
||||||
// TODO: This is not OK since it's creating a new array every time, so triggering a
|
|
||||||
// component rendering.
|
|
||||||
const peersArray = Object.values(state.peers);
|
const peersArray = Object.values(state.peers);
|
||||||
|
const videoStreamsArray = Object.values(state.consumers);
|
||||||
|
const videoStreams =
|
||||||
|
videoStreamsArray.filter((consumer) =>
|
||||||
|
{
|
||||||
|
return (consumer.source === 'webcam' || consumer.source === 'screen');
|
||||||
|
}).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peers : peersArray,
|
peers : peersArray,
|
||||||
|
videoStreams : videoStreams,
|
||||||
activeSpeakerName : state.room.activeSpeakerName,
|
activeSpeakerName : state.room.activeSpeakerName,
|
||||||
peerHeight : state.room.peerHeight,
|
toolAreaOpen : state.toolarea.toolAreaOpen
|
||||||
peerWidth : state.room.peerWidth
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const PeersContainer = connect(
|
const PeersContainer = connect(
|
||||||
mapStateToProps,
|
mapStateToProps
|
||||||
mapDispatchToProps
|
|
||||||
)(Peers);
|
)(Peers);
|
||||||
|
|
||||||
export default PeersContainer;
|
export default PeersContainer;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ import { Appear } from './transitions';
|
||||||
import Me from './Me';
|
import Me from './Me';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
import Notifications from './Notifications';
|
import Notifications from './Notifications';
|
||||||
import ChatWidget from './ChatWidget';
|
import ToolAreaButton from './ToolArea/ToolAreaButton';
|
||||||
|
import ToolArea from './ToolArea/ToolArea';
|
||||||
|
import FullScreenView from './FullScreenView';
|
||||||
|
import Draggable from 'react-draggable';
|
||||||
|
|
||||||
class Room extends React.Component
|
class Room extends React.Component
|
||||||
{
|
{
|
||||||
|
|
@ -19,15 +22,15 @@ class Room extends React.Component
|
||||||
const {
|
const {
|
||||||
room,
|
room,
|
||||||
me,
|
me,
|
||||||
|
toolAreaOpen,
|
||||||
amActiveSpeaker,
|
amActiveSpeaker,
|
||||||
screenProducer,
|
screenProducer,
|
||||||
onRoomLinkCopy,
|
onRoomLinkCopy,
|
||||||
onSetAudioMode,
|
onLogin,
|
||||||
onRestartIce,
|
|
||||||
onLeaveMeeting,
|
|
||||||
onShareScreen,
|
onShareScreen,
|
||||||
onUnShareScreen,
|
onUnShareScreen,
|
||||||
onNeedExtension
|
onNeedExtension,
|
||||||
|
onLeaveMeeting
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let screenState;
|
let screenState;
|
||||||
|
|
@ -57,13 +60,23 @@ class Room extends React.Component
|
||||||
return (
|
return (
|
||||||
<Appear duration={300}>
|
<Appear duration={300}>
|
||||||
<div data-component='Room'>
|
<div data-component='Room'>
|
||||||
|
<FullScreenView advancedMode={room.advancedMode} />
|
||||||
|
<div
|
||||||
|
className='room-wrapper'
|
||||||
|
style={{
|
||||||
|
width : toolAreaOpen ? '80%' : '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ChatWidget />
|
<ToolAreaButton />
|
||||||
|
|
||||||
|
{room.advancedMode ?
|
||||||
<div className='state' data-tip='Server status'>
|
<div className='state' data-tip='Server status'>
|
||||||
<div className={classnames('icon', room.state)} />
|
<div className={classnames('icon', room.state)} />
|
||||||
<p className={classnames('text', room.state)}>{room.state}</p>
|
<p className={classnames('text', room.state)}>{room.state}</p>
|
||||||
</div>
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
<div className='room-link-wrapper'>
|
<div className='room-link-wrapper'>
|
||||||
<div className='room-link'>
|
<div className='room-link'>
|
||||||
|
|
@ -96,15 +109,21 @@ class Room extends React.Component
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Peers />
|
<Peers
|
||||||
|
advancedMode={room.advancedMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
|
||||||
<div
|
<div
|
||||||
className={classnames('me-container', {
|
className={classnames('me-container', {
|
||||||
'active-speaker' : amActiveSpeaker
|
'active-speaker' : amActiveSpeaker
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Me />
|
<Me
|
||||||
|
advancedMode={room.advancedMode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
<div className='sidebar'>
|
<div className='sidebar'>
|
||||||
<div
|
<div
|
||||||
|
|
@ -138,24 +157,17 @@ class Room extends React.Component
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{me.loginEnabled ?
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'audio-only', {
|
className={classnames('button', 'login', 'off', {
|
||||||
on : me.audioOnly,
|
disabled : me.loginInProgress
|
||||||
disabled : me.audioOnlyInProgress
|
|
||||||
})}
|
})}
|
||||||
data-tip='Toggle audio only mode'
|
data-tip='Login'
|
||||||
data-type='dark'
|
data-type='dark'
|
||||||
onClick={() => onSetAudioMode(!me.audioOnly)}
|
onClick={() => onLogin()}
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classnames('button', 'restart-ice', {
|
|
||||||
disabled : me.restartIceInProgress
|
|
||||||
})}
|
|
||||||
data-tip='Restart ICE'
|
|
||||||
data-type='dark'
|
|
||||||
onClick={() => onRestartIce()}
|
|
||||||
/>
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classnames('button', 'leave-meeting')}
|
className={classnames('button', 'leave-meeting')}
|
||||||
|
|
@ -171,6 +183,20 @@ class Room extends React.Component
|
||||||
delayHide={100}
|
delayHide={100}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className='toolarea-wrapper'
|
||||||
|
style={{
|
||||||
|
width : toolAreaOpen ? '20%' : '0%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toolAreaOpen ?
|
||||||
|
<ToolArea
|
||||||
|
advancedMode={room.advancedMode}
|
||||||
|
/>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Appear>
|
</Appear>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -181,14 +207,14 @@ Room.propTypes =
|
||||||
room : appPropTypes.Room.isRequired,
|
room : appPropTypes.Room.isRequired,
|
||||||
me : appPropTypes.Me.isRequired,
|
me : appPropTypes.Me.isRequired,
|
||||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||||
|
toolAreaOpen : PropTypes.bool.isRequired,
|
||||||
screenProducer : appPropTypes.Producer,
|
screenProducer : appPropTypes.Producer,
|
||||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||||
onSetAudioMode : PropTypes.func.isRequired,
|
|
||||||
onRestartIce : PropTypes.func.isRequired,
|
|
||||||
onLeaveMeeting : PropTypes.func.isRequired,
|
|
||||||
onShareScreen : PropTypes.func.isRequired,
|
onShareScreen : PropTypes.func.isRequired,
|
||||||
onUnShareScreen : PropTypes.func.isRequired,
|
onUnShareScreen : PropTypes.func.isRequired,
|
||||||
onNeedExtension : PropTypes.func.isRequired
|
onNeedExtension : PropTypes.func.isRequired,
|
||||||
|
onLeaveMeeting : PropTypes.func.isRequired,
|
||||||
|
onLogin : PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state) =>
|
const mapStateToProps = (state) =>
|
||||||
|
|
@ -200,6 +226,7 @@ const mapStateToProps = (state) =>
|
||||||
return {
|
return {
|
||||||
room : state.room,
|
room : state.room,
|
||||||
me : state.me,
|
me : state.me,
|
||||||
|
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
|
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
|
||||||
screenProducer : screenProducer
|
screenProducer : screenProducer
|
||||||
};
|
};
|
||||||
|
|
@ -215,17 +242,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
text : 'Room link copied to the clipboard'
|
text : 'Room link copied to the clipboard'
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
onSetAudioMode : (enable) =>
|
|
||||||
{
|
|
||||||
if (enable)
|
|
||||||
dispatch(requestActions.enableAudioOnly());
|
|
||||||
else
|
|
||||||
dispatch(requestActions.disableAudioOnly());
|
|
||||||
},
|
|
||||||
onRestartIce : () =>
|
|
||||||
{
|
|
||||||
dispatch(requestActions.restartIce());
|
|
||||||
},
|
|
||||||
onLeaveMeeting : () =>
|
onLeaveMeeting : () =>
|
||||||
{
|
{
|
||||||
dispatch(requestActions.leaveRoom());
|
dispatch(requestActions.leaveRoom());
|
||||||
|
|
@ -241,6 +257,10 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
onNeedExtension : () =>
|
onNeedExtension : () =>
|
||||||
{
|
{
|
||||||
dispatch(requestActions.installExtension());
|
dispatch(requestActions.installExtension());
|
||||||
|
},
|
||||||
|
onLogin : () =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.userLogin());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Spinner from 'react-spinner';
|
||||||
|
|
||||||
|
export default class PeerView extends React.Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state =
|
||||||
|
{
|
||||||
|
screenWidth : null,
|
||||||
|
screenHeight : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Latest received screen track.
|
||||||
|
// @type {MediaStreamTrack}
|
||||||
|
this._screenTrack = null;
|
||||||
|
|
||||||
|
// Periodic timer for showing video resolution.
|
||||||
|
this._screenResolutionTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
isMe,
|
||||||
|
advancedMode,
|
||||||
|
screenVisible,
|
||||||
|
screenProfile,
|
||||||
|
screenCodec
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
screenWidth,
|
||||||
|
screenHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='ScreenView'>
|
||||||
|
<div className='info'>
|
||||||
|
{advancedMode ?
|
||||||
|
<div className={classnames('media', { 'is-me': isMe })}>
|
||||||
|
{screenVisible ?
|
||||||
|
<div className='box'>
|
||||||
|
{screenCodec ?
|
||||||
|
<p className='codec'>{screenCodec} {screenProfile}</p>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
|
||||||
|
{(screenVisible && screenWidth !== null) ?
|
||||||
|
<p className='resolution'>{screenWidth}x{screenHeight}</p>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<video
|
||||||
|
ref='video'
|
||||||
|
className={classnames({
|
||||||
|
hidden : !screenVisible,
|
||||||
|
'is-me' : isMe,
|
||||||
|
loading : screenProfile === 'none'
|
||||||
|
})}
|
||||||
|
autoPlay
|
||||||
|
muted={Boolean(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{screenProfile === 'none' ?
|
||||||
|
<div className='spinner-container'>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
:null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount()
|
||||||
|
{
|
||||||
|
const { screenTrack } = this.props;
|
||||||
|
|
||||||
|
this._setTracks(screenTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount()
|
||||||
|
{
|
||||||
|
clearInterval(this._screenResolutionTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps)
|
||||||
|
{
|
||||||
|
const { screenTrack } = nextProps;
|
||||||
|
|
||||||
|
this._setTracks(screenTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setTracks(screenTrack)
|
||||||
|
{
|
||||||
|
if (this._screenTrack === screenTrack)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._screenTrack = screenTrack;
|
||||||
|
|
||||||
|
clearInterval(this._screenResolutionTimer);
|
||||||
|
this._hideScreenResolution();
|
||||||
|
|
||||||
|
const { video } = this.refs;
|
||||||
|
|
||||||
|
if (screenTrack)
|
||||||
|
{
|
||||||
|
const stream = new MediaStream;
|
||||||
|
|
||||||
|
if (screenTrack)
|
||||||
|
stream.addTrack(screenTrack);
|
||||||
|
|
||||||
|
video.srcObject = stream;
|
||||||
|
|
||||||
|
if (screenTrack)
|
||||||
|
this._showScreenResolution();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
video.srcObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showScreenResolution()
|
||||||
|
{
|
||||||
|
this._screenResolutionTimer = setInterval(() =>
|
||||||
|
{
|
||||||
|
const { screenWidth, screenHeight } = this.state;
|
||||||
|
const { video } = this.refs;
|
||||||
|
|
||||||
|
// Don't re-render if nothing changed.
|
||||||
|
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
screenWidth : video.videoWidth,
|
||||||
|
screenHeight : video.videoHeight
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideScreenResolution()
|
||||||
|
{
|
||||||
|
this.setState({ screenWidth: null, screenHeight: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PeerView.propTypes =
|
||||||
|
{
|
||||||
|
isMe : PropTypes.bool,
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
screenTrack : PropTypes.any,
|
||||||
|
screenVisible : PropTypes.bool,
|
||||||
|
screenProfile : PropTypes.string,
|
||||||
|
screenCodec : PropTypes.string
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as appPropTypes from './appPropTypes';
|
||||||
|
import * as requestActions from '../redux/requestActions';
|
||||||
|
import * as stateActions from '../redux/stateActions';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Dropdown from 'react-dropdown';
|
||||||
|
|
||||||
|
class Settings extends React.Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
room,
|
||||||
|
me,
|
||||||
|
handleChangeWebcam,
|
||||||
|
handleChangeAudioDevice,
|
||||||
|
onToggleAdvancedMode
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let webcams;
|
||||||
|
let webcamText;
|
||||||
|
|
||||||
|
if (me.canChangeWebcam)
|
||||||
|
webcamText = 'Select camera';
|
||||||
|
else
|
||||||
|
webcamText = 'Unable to select camera';
|
||||||
|
|
||||||
|
if (me.webcamDevices)
|
||||||
|
webcams = Array.from(me.webcamDevices.values());
|
||||||
|
else
|
||||||
|
webcams = [];
|
||||||
|
|
||||||
|
let audioDevices;
|
||||||
|
let audioDevicesText;
|
||||||
|
|
||||||
|
if (me.canChangeAudioDevice)
|
||||||
|
audioDevicesText = 'Select audio input device';
|
||||||
|
else
|
||||||
|
audioDevicesText = 'Unable to select audio input device';
|
||||||
|
|
||||||
|
if (me.audioDevices)
|
||||||
|
audioDevices = Array.from(me.audioDevices.values());
|
||||||
|
else
|
||||||
|
audioDevices = [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='Settings'>
|
||||||
|
<div className='settings'>
|
||||||
|
<Dropdown
|
||||||
|
disabled={!me.canChangeWebcam}
|
||||||
|
options={webcams}
|
||||||
|
onChange={handleChangeWebcam}
|
||||||
|
placeholder={webcamText}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
disabled={!me.canChangeAudioDevice}
|
||||||
|
options={audioDevices}
|
||||||
|
onChange={handleChangeAudioDevice}
|
||||||
|
placeholder={audioDevicesText}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
defaultChecked={room.advancedMode}
|
||||||
|
onChange={onToggleAdvancedMode}
|
||||||
|
/>
|
||||||
|
<span>Advanced mode</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.propTypes =
|
||||||
|
{
|
||||||
|
me : appPropTypes.Me.isRequired,
|
||||||
|
room : appPropTypes.Room.isRequired,
|
||||||
|
handleChangeWebcam : PropTypes.func.isRequired,
|
||||||
|
handleChangeAudioDevice : PropTypes.func.isRequired,
|
||||||
|
onToggleAdvancedMode : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
me : state.me,
|
||||||
|
room : state.room
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
handleChangeWebcam : (device) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.changeWebcam(device.value));
|
||||||
|
},
|
||||||
|
handleChangeAudioDevice : (device) =>
|
||||||
|
{
|
||||||
|
dispatch(requestActions.changeAudioDevice(device.value));
|
||||||
|
},
|
||||||
|
onToggleAdvancedMode : () =>
|
||||||
|
{
|
||||||
|
dispatch(stateActions.toggleAdvancedMode());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Settings);
|
||||||
|
|
||||||
|
export default SettingsContainer;
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as stateActions from '../../redux/stateActions';
|
||||||
|
import ParticipantList from '../ParticipantList/ParticipantList';
|
||||||
|
import Chat from '../Chat/Chat';
|
||||||
|
import Settings from '../Settings';
|
||||||
|
|
||||||
|
class ToolArea extends React.Component
|
||||||
|
{
|
||||||
|
constructor(props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
toolarea,
|
||||||
|
setToolTab
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='ToolArea'>
|
||||||
|
<div className='tabs'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='tabs'
|
||||||
|
id='tab-chat'
|
||||||
|
onChange={() =>
|
||||||
|
{
|
||||||
|
setToolTab('chat');
|
||||||
|
}}
|
||||||
|
checked={toolarea.currentToolTab === 'chat'}
|
||||||
|
/>
|
||||||
|
<label htmlFor='tab-chat'>Chat</label>
|
||||||
|
|
||||||
|
<div className='tab'>
|
||||||
|
<Chat />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='tabs'
|
||||||
|
id='tab-users'
|
||||||
|
onChange={() =>
|
||||||
|
{
|
||||||
|
setToolTab('users');
|
||||||
|
}}
|
||||||
|
checked={toolarea.currentToolTab === 'users'}
|
||||||
|
/>
|
||||||
|
<label htmlFor='tab-users'>Users</label>
|
||||||
|
|
||||||
|
<div className='tab'>
|
||||||
|
<ParticipantList />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='tabs'
|
||||||
|
id='tab-settings'
|
||||||
|
onChange={() =>
|
||||||
|
{
|
||||||
|
setToolTab('settings');
|
||||||
|
}}
|
||||||
|
checked={toolarea.currentToolTab === 'settings'}
|
||||||
|
/>
|
||||||
|
<label htmlFor='tab-settings'>Settings</label>
|
||||||
|
|
||||||
|
<div className='tab'>
|
||||||
|
<Settings />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolArea.propTypes =
|
||||||
|
{
|
||||||
|
advancedMode : PropTypes.bool,
|
||||||
|
toolarea : PropTypes.object.isRequired,
|
||||||
|
setToolTab : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
toolarea : state.toolarea
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
setToolTab : (toolTab) =>
|
||||||
|
{
|
||||||
|
dispatch(stateActions.setToolTab(toolTab));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolAreaContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ToolArea);
|
||||||
|
|
||||||
|
export default ToolAreaContainer;
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import * as stateActions from '../../redux/stateActions';
|
||||||
|
|
||||||
|
class ToolAreaButton extends React.Component
|
||||||
|
{
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
toolAreaOpen,
|
||||||
|
toggleToolArea
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component='ToolAreaButton'>
|
||||||
|
<div
|
||||||
|
className={classnames('button', 'toolarea-button', {
|
||||||
|
on : toolAreaOpen
|
||||||
|
})}
|
||||||
|
data-tip='Toggle tool area'
|
||||||
|
data-type='dark'
|
||||||
|
data-for='globaltip'
|
||||||
|
onClick={() => toggleToolArea()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolAreaButton.propTypes =
|
||||||
|
{
|
||||||
|
toolAreaOpen : PropTypes.bool.isRequired,
|
||||||
|
toggleToolArea : PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
toolAreaOpen : state.toolarea.toolAreaOpen
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
toggleToolArea : () =>
|
||||||
|
{
|
||||||
|
dispatch(stateActions.toggleToolArea());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolAreaButtonContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ToolAreaButton);
|
||||||
|
|
||||||
|
export default ToolAreaButtonContainer;
|
||||||
|
|
@ -5,7 +5,8 @@ import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
applyMiddleware as applyReduxMiddleware,
|
applyMiddleware as applyReduxMiddleware,
|
||||||
createStore as createReduxStore
|
createStore as createReduxStore,
|
||||||
|
compose as composeRedux
|
||||||
} from 'redux';
|
} from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { createLogger as createReduxLogger } from 'redux-logger';
|
import { createLogger as createReduxLogger } from 'redux-logger';
|
||||||
|
|
@ -19,6 +20,7 @@ import * as stateActions from './redux/stateActions';
|
||||||
import reducers from './redux/reducers';
|
import reducers from './redux/reducers';
|
||||||
import roomClientMiddleware from './redux/roomClientMiddleware';
|
import roomClientMiddleware from './redux/roomClientMiddleware';
|
||||||
import Room from './components/Room';
|
import Room from './components/Room';
|
||||||
|
import { loginEnabled } from '../config';
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
const reduxMiddlewares =
|
const reduxMiddlewares =
|
||||||
|
|
@ -40,10 +42,22 @@ if (process.env.NODE_ENV === 'development')
|
||||||
reduxMiddlewares.push(reduxLogger);
|
reduxMiddlewares.push(reduxLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const composeEnhancers =
|
||||||
|
typeof window === 'object' &&
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||||
|
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||||
|
}) : composeRedux;
|
||||||
|
|
||||||
|
const enhancer = composeEnhancers(
|
||||||
|
applyReduxMiddleware(...reduxMiddlewares)
|
||||||
|
// other store enhancers if any
|
||||||
|
);
|
||||||
|
|
||||||
const store = createReduxStore(
|
const store = createReduxStore(
|
||||||
reducers,
|
reducers,
|
||||||
undefined,
|
undefined,
|
||||||
applyReduxMiddleware(...reduxMiddlewares)
|
enhancer
|
||||||
);
|
);
|
||||||
|
|
||||||
domready(() =>
|
domready(() =>
|
||||||
|
|
@ -61,11 +75,12 @@ function run()
|
||||||
|
|
||||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||||
const urlParser = new UrlParse(window.location.href, true);
|
const urlParser = new UrlParse(window.location.href, true);
|
||||||
let roomId = urlParser.query.roomId;
|
let roomId = (urlParser.pathname).substr(1)
|
||||||
|
? (urlParser.pathname).substr(1) : urlParser.query.roomId;
|
||||||
const produce = urlParser.query.produce !== 'false';
|
const produce = urlParser.query.produce !== 'false';
|
||||||
let displayName = urlParser.query.displayName;
|
let displayName = urlParser.query.displayName;
|
||||||
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
||||||
const useSimulcast = urlParser.query.simulcast !== 'false';
|
const useSimulcast = urlParser.query.simulcast === 'true';
|
||||||
|
|
||||||
if (!roomId)
|
if (!roomId)
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +143,7 @@ function run()
|
||||||
|
|
||||||
// NOTE: I don't like this.
|
// NOTE: I don't like this.
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
stateActions.setMe({ peerName, displayName, displayNameSet, device }));
|
stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled }));
|
||||||
|
|
||||||
// NOTE: I don't like this.
|
// NOTE: I don't like this.
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
{
|
{
|
||||||
name : 'alice',
|
name : 'alice',
|
||||||
displayName : 'Alice Thomsom',
|
displayName : 'Alice Thomsom',
|
||||||
|
raiseHandState : false,
|
||||||
device : { flag: 'chrome', name: 'Chrome', version: '58' },
|
device : { flag: 'chrome', name: 'Chrome', version: '58' },
|
||||||
consumers : [ 5551, 5552 ]
|
consumers : [ 5551, 5552 ]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ const chatbehavior = (state = initialState, action) =>
|
||||||
case 'TOGGLE_CHAT':
|
case 'TOGGLE_CHAT':
|
||||||
{
|
{
|
||||||
const showChat = !state.showChat;
|
const showChat = !state.showChat;
|
||||||
|
const badge = 0;
|
||||||
|
|
||||||
return { ...state, showChat };
|
return { ...state, showChat, badge };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'TOGGLE_INPUT_DISABLED':
|
case 'TOGGLE_INPUT_DISABLED':
|
||||||
|
|
@ -23,6 +24,10 @@ const chatbehavior = (state = initialState, action) =>
|
||||||
return { ...state, disabledInput };
|
return { ...state, disabledInput };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'INCREASE_BADGE':
|
||||||
|
{
|
||||||
|
return { ...state, badge: state.badge + (state.showChat ? 0 : 1) };
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import consumers from './consumers';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import chatmessages from './chatmessages';
|
import chatmessages from './chatmessages';
|
||||||
import chatbehavior from './chatbehavior';
|
import chatbehavior from './chatbehavior';
|
||||||
|
import toolarea from './toolarea';
|
||||||
|
|
||||||
const reducers = combineReducers(
|
const reducers = combineReducers(
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +18,8 @@ const reducers = combineReducers(
|
||||||
consumers,
|
consumers,
|
||||||
notifications,
|
notifications,
|
||||||
chatmessages,
|
chatmessages,
|
||||||
chatbehavior
|
chatbehavior,
|
||||||
|
toolarea
|
||||||
});
|
});
|
||||||
|
|
||||||
export default reducers;
|
export default reducers;
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,19 @@ const initialState =
|
||||||
canSendWebcam : false,
|
canSendWebcam : false,
|
||||||
canShareScreen : false,
|
canShareScreen : false,
|
||||||
needExtension : false,
|
needExtension : false,
|
||||||
|
canChangeAudioDevice : false,
|
||||||
|
audioDevices : null,
|
||||||
canChangeWebcam : false,
|
canChangeWebcam : false,
|
||||||
|
webcamDevices : null,
|
||||||
webcamInProgress : false,
|
webcamInProgress : false,
|
||||||
|
audioInProgress : false,
|
||||||
screenShareInProgress : false,
|
screenShareInProgress : false,
|
||||||
|
loginInProgress : false,
|
||||||
|
loginEnabled : false,
|
||||||
audioOnly : false,
|
audioOnly : false,
|
||||||
audioOnlyInProgress : false,
|
audioOnlyInProgress : false,
|
||||||
|
raiseHand : false,
|
||||||
|
raiseHandInProgress : false,
|
||||||
restartIceInProgress : false
|
restartIceInProgress : false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -22,9 +30,22 @@ const me = (state = initialState, action) =>
|
||||||
{
|
{
|
||||||
case 'SET_ME':
|
case 'SET_ME':
|
||||||
{
|
{
|
||||||
const { peerName, displayName, displayNameSet, device } = action.payload;
|
const {
|
||||||
|
peerName,
|
||||||
|
displayName,
|
||||||
|
displayNameSet,
|
||||||
|
device,
|
||||||
|
loginEnabled
|
||||||
|
} = action.payload;
|
||||||
|
|
||||||
return { ...state, name: peerName, displayName, displayNameSet, device };
|
return {
|
||||||
|
...state,
|
||||||
|
name : peerName,
|
||||||
|
displayName,
|
||||||
|
displayNameSet,
|
||||||
|
device,
|
||||||
|
loginEnabled
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_MEDIA_CAPABILITIES':
|
case 'SET_MEDIA_CAPABILITIES':
|
||||||
|
|
@ -41,6 +62,20 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, canShareScreen, needExtension };
|
return { ...state, canShareScreen, needExtension };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_CAN_CHANGE_AUDIO_DEVICE':
|
||||||
|
{
|
||||||
|
const canChangeAudioDevice = action.payload;
|
||||||
|
|
||||||
|
return { ...state, canChangeAudioDevice };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_AUDIO_DEVICES':
|
||||||
|
{
|
||||||
|
const { devices } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, audioDevices: devices };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_CAN_CHANGE_WEBCAM':
|
case 'SET_CAN_CHANGE_WEBCAM':
|
||||||
{
|
{
|
||||||
const canChangeWebcam = action.payload;
|
const canChangeWebcam = action.payload;
|
||||||
|
|
@ -48,6 +83,20 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, canChangeWebcam };
|
return { ...state, canChangeWebcam };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_WEBCAM_DEVICES':
|
||||||
|
{
|
||||||
|
const { devices } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, webcamDevices: devices };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_AUDIO_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { flag } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, audioInProgress: flag };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_WEBCAM_IN_PROGRESS':
|
case 'SET_WEBCAM_IN_PROGRESS':
|
||||||
{
|
{
|
||||||
const { flag } = action.payload;
|
const { flag } = action.payload;
|
||||||
|
|
@ -62,6 +111,13 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, screenShareInProgress: flag };
|
return { ...state, screenShareInProgress: flag };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_LOGIN_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { flag } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, loginInProgress: flag };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_DISPLAY_NAME':
|
case 'SET_DISPLAY_NAME':
|
||||||
{
|
{
|
||||||
let { displayName } = action.payload;
|
let { displayName } = action.payload;
|
||||||
|
|
@ -87,6 +143,20 @@ const me = (state = initialState, action) =>
|
||||||
return { ...state, audioOnlyInProgress: flag };
|
return { ...state, audioOnlyInProgress: flag };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_MY_RAISE_HAND_STATE':
|
||||||
|
{
|
||||||
|
const { flag } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, raiseHand: flag };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { flag } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, raiseHandInProgress: flag };
|
||||||
|
}
|
||||||
|
|
||||||
case 'SET_RESTART_ICE_IN_PROGRESS':
|
case 'SET_RESTART_ICE_IN_PROGRESS':
|
||||||
{
|
{
|
||||||
const { flag } = action.payload;
|
const { flag } = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,58 @@ const peers = (state = initialState, action) =>
|
||||||
return { ...state, [newPeer.name]: newPeer };
|
return { ...state, [newPeer.name]: newPeer };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { peerName, flag } = action.payload;
|
||||||
|
const peer = state[peerName];
|
||||||
|
|
||||||
|
if (!peer)
|
||||||
|
throw new Error('no Peer found');
|
||||||
|
|
||||||
|
const newPeer = { ...peer, peerVideoInProgress: flag };
|
||||||
|
|
||||||
|
return { ...state, [newPeer.name]: newPeer };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { peerName, flag } = action.payload;
|
||||||
|
const peer = state[peerName];
|
||||||
|
|
||||||
|
if (!peer)
|
||||||
|
throw new Error('no Peer found');
|
||||||
|
|
||||||
|
const newPeer = { ...peer, peerAudioInProgress: flag };
|
||||||
|
|
||||||
|
return { ...state, [newPeer.name]: newPeer };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||||
|
{
|
||||||
|
const { peerName, flag } = action.payload;
|
||||||
|
const peer = state[peerName];
|
||||||
|
|
||||||
|
if (!peer)
|
||||||
|
throw new Error('no Peer found');
|
||||||
|
|
||||||
|
const newPeer = { ...peer, peerScreenInProgress: flag };
|
||||||
|
|
||||||
|
return { ...state, [newPeer.name]: newPeer };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PEER_RAISE_HAND_STATE':
|
||||||
|
{
|
||||||
|
const { peerName, raiseHandState } = action.payload;
|
||||||
|
const peer = state[peerName];
|
||||||
|
|
||||||
|
if (!peer)
|
||||||
|
throw new Error('no Peer found');
|
||||||
|
|
||||||
|
const newPeer = { ...peer, raiseHandState };
|
||||||
|
|
||||||
|
return { ...state, [newPeer.name]: newPeer };
|
||||||
|
}
|
||||||
|
|
||||||
case 'ADD_CONSUMER':
|
case 'ADD_CONSUMER':
|
||||||
{
|
{
|
||||||
const { consumer, peerName } = action.payload;
|
const { consumer, peerName } = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ const initialState =
|
||||||
url : null,
|
url : null,
|
||||||
state : 'new', // new/connecting/connected/disconnected/closed,
|
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||||
activeSpeakerName : null,
|
activeSpeakerName : null,
|
||||||
peerHeight : 300,
|
showSettings : false,
|
||||||
peerWidth : 400
|
advancedMode : false,
|
||||||
|
fullScreenConsumer : null // ConsumerID
|
||||||
};
|
};
|
||||||
|
|
||||||
const room = (state = initialState, action) =>
|
const room = (state = initialState, action) =>
|
||||||
|
|
@ -35,11 +36,26 @@ const room = (state = initialState, action) =>
|
||||||
return { ...state, activeSpeakerName: peerName };
|
return { ...state, activeSpeakerName: peerName };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SET_COMPONENT_SIZE':
|
case 'TOGGLE_SETTINGS':
|
||||||
{
|
{
|
||||||
const { peerWidth, peerHeight } = action.payload;
|
const showSettings = !state.showSettings;
|
||||||
|
|
||||||
return { ...state, peerWidth: peerWidth, peerHeight: peerHeight };
|
return { ...state, showSettings };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'TOGGLE_ADVANCED_MODE':
|
||||||
|
{
|
||||||
|
const advancedMode = !state.advancedMode;
|
||||||
|
|
||||||
|
return { ...state, advancedMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'TOGGLE_FULLSCREEN_CONSUMER':
|
||||||
|
{
|
||||||
|
const { consumerId } = action.payload;
|
||||||
|
const currentConsumer = state.fullScreenConsumer;
|
||||||
|
|
||||||
|
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
const initialState =
|
||||||
|
{
|
||||||
|
toolAreaOpen : false,
|
||||||
|
currentToolTab : 'chat' // chat, settings, users
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolarea = (state = initialState, action) =>
|
||||||
|
{
|
||||||
|
switch (action.type)
|
||||||
|
{
|
||||||
|
case 'TOGGLE_TOOL_AREA':
|
||||||
|
{
|
||||||
|
const toolAreaOpen = !state.toolAreaOpen;
|
||||||
|
|
||||||
|
return { ...state, toolAreaOpen };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_TOOL_TAB':
|
||||||
|
{
|
||||||
|
const { toolTab } = action.payload;
|
||||||
|
|
||||||
|
return { ...state, currentToolTab: toolTab };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toolarea;
|
||||||
|
|
@ -57,10 +57,19 @@ export const disableWebcam = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeWebcam = () =>
|
export const changeWebcam = (deviceId) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
type : 'CHANGE_WEBCAM'
|
type : 'CHANGE_WEBCAM',
|
||||||
|
payload : { deviceId }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeAudioDevice = (deviceId) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'CHANGE_AUDIO_DEVICE',
|
||||||
|
payload : { deviceId }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -78,6 +87,75 @@ export const disableAudioOnly = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mutePeerAudio = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'MUTE_PEER_AUDIO',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unmutePeerAudio = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'UNMUTE_PEER_AUDIO',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pausePeerVideo = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'PAUSE_PEER_VIDEO',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resumePeerVideo = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'RESUME_PEER_VIDEO',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pausePeerScreen = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'PAUSE_PEER_SCREEN',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resumePeerScreen = (peerName) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'RESUME_PEER_SCREEN',
|
||||||
|
payload : { peerName }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userLogin = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'USER_LOGIN'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const raiseHand = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'RAISE_HAND'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lowerHand = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'LOWER_HAND'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const restartIce = () =>
|
export const restartIce = () =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,18 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
|
|
||||||
case 'CHANGE_WEBCAM':
|
case 'CHANGE_WEBCAM':
|
||||||
{
|
{
|
||||||
client.changeWebcam();
|
const { deviceId } = action.payload;
|
||||||
|
|
||||||
|
client.changeWebcam(deviceId);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CHANGE_AUDIO_DEVICE':
|
||||||
|
{
|
||||||
|
const { deviceId } = action.payload;
|
||||||
|
|
||||||
|
client.changeAudioDevice(deviceId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +113,81 @@ export default ({ dispatch, getState }) => (next) =>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'MUTE_PEER_AUDIO':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.mutePeerAudio(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UNMUTE_PEER_AUDIO':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.unmutePeerAudio(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'PAUSE_PEER_VIDEO':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.pausePeerVideo(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RESUME_PEER_VIDEO':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.resumePeerVideo(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'PAUSE_PEER_SCREEN':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.pausePeerScreen(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RESUME_PEER_SCREEN':
|
||||||
|
{
|
||||||
|
const { peerName } = action.payload;
|
||||||
|
|
||||||
|
client.resumePeerScreen(peerName);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RAISE_HAND':
|
||||||
|
{
|
||||||
|
client.sendRaiseHandState(true);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'USER_LOGIN':
|
||||||
|
{
|
||||||
|
client.login();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'LOWER_HAND':
|
||||||
|
{
|
||||||
|
client.sendRaiseHandState(false);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'RESTART_ICE':
|
case 'RESTART_ICE':
|
||||||
{
|
{
|
||||||
client.restartIce();
|
client.restartIce();
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,11 @@ export const setRoomActiveSpeaker = (peerName) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onComponentResize = (peerWidth, peerHeight) =>
|
export const setMe = ({ peerName, displayName, displayNameSet, device, loginEnabled }) =>
|
||||||
{
|
|
||||||
return {
|
|
||||||
type : 'SET_COMPONENT_SIZE',
|
|
||||||
payload : { peerWidth, peerHeight }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setMe = ({ peerName, displayName, displayNameSet, device }) =>
|
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
type : 'SET_ME',
|
type : 'SET_ME',
|
||||||
payload : { peerName, displayName, displayNameSet, device }
|
payload : { peerName, displayName, displayNameSet, device, loginEnabled }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,6 +46,22 @@ export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setCanChangeAudioDevice = (flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_CAN_CHANGE_AUDIO_DEVICE',
|
||||||
|
payload : flag
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAudioDevices = (devices) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_AUDIO_DEVICES',
|
||||||
|
payload : { devices }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setCanChangeWebcam = (flag) =>
|
export const setCanChangeWebcam = (flag) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -62,6 +70,14 @@ export const setCanChangeWebcam = (flag) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setWebcamDevices = (devices) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_WEBCAM_DEVICES',
|
||||||
|
payload : { devices }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setDisplayName = (displayName) =>
|
export const setDisplayName = (displayName) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -70,6 +86,13 @@ export const setDisplayName = (displayName) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleAdvancedMode = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'TOGGLE_ADVANCED_MODE'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setAudioOnlyState = (enabled) =>
|
export const setAudioOnlyState = (enabled) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -86,6 +109,84 @@ export const setAudioOnlyInProgress = (flag) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setPeerVideoInProgress = (peerName, flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_PEER_VIDEO_IN_PROGRESS',
|
||||||
|
payload : { peerName, flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPeerAudioInProgress = (peerName, flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_PEER_AUDIO_IN_PROGRESS',
|
||||||
|
payload : { peerName, flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPeerScreenInProgress = (peerName, flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_PEER_SCREEN_IN_PROGRESS',
|
||||||
|
payload : { peerName, flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMyRaiseHandState = (flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_MY_RAISE_HAND_STATE',
|
||||||
|
payload : { flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLoginInProgress = (flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_LOGIN_IN_PROGRESS',
|
||||||
|
payload : { flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleSettings = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'TOGGLE_SETTINGS'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleToolArea = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'TOGGLE_TOOL_AREA'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setToolTab = (toolTab) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_TOOL_TAB',
|
||||||
|
payload : { toolTab }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMyRaiseHandStateInProgress = (flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
|
||||||
|
payload : { flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_PEER_RAISE_HAND_STATE',
|
||||||
|
payload : { peerName, raiseHandState }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setRestartIceInProgress = (flag) =>
|
export const setRestartIceInProgress = (flag) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -134,6 +235,14 @@ export const setProducerTrack = (producerId, track) =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setAudioInProgress = (flag) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'SET_AUDIO_IN_PROGRESS',
|
||||||
|
payload : { flag }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const setWebcamInProgress = (flag) =>
|
export const setWebcamInProgress = (flag) =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
@ -252,6 +361,21 @@ export const toggleChat = () =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleConsumerFullscreen = (consumerId) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'TOGGLE_FULLSCREEN_CONSUMER',
|
||||||
|
payload : { consumerId }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const increaseBadge = () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
type : 'INCREASE_BADGE'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const toggleInputDisabled = () =>
|
export const toggleInputDisabled = () =>
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -35,5 +35,11 @@ export function getBrowserType()
|
||||||
return 'chrome';
|
return 'chrome';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MSEdge
|
||||||
|
if (ua.indexOf('edge') !== -1)
|
||||||
|
{
|
||||||
|
return 'edge';
|
||||||
|
}
|
||||||
|
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,15 @@
|
||||||
"hark": "^1.1.6",
|
"hark": "^1.1.6",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"marked": "^0.3.17",
|
"marked": "^0.3.17",
|
||||||
"mediasoup-client": "^2.0.14",
|
"mediasoup-client": "^2.1.1",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"protoo-client": "^2.0.7",
|
"protoo-client": "^2.0.7",
|
||||||
"random-string": "^0.2.0",
|
"random-string": "^0.2.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
"react-clipboard.js": "^1.1.3",
|
"react-clipboard.js": "^1.1.3",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.2.0",
|
||||||
|
"react-draggable": "^3.0.5",
|
||||||
|
"react-dropdown": "^1.5.0",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"react-spinner": "^0.2.7",
|
"react-spinner": "^0.2.7",
|
||||||
"react-tooltip": "^3.4.0",
|
"react-tooltip": "^3.4.0",
|
||||||
|
|
@ -38,6 +40,7 @@
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
"babelify": "^8.0.0",
|
"babelify": "^8.0.0",
|
||||||
"browser-sync": "^2.23.6",
|
"browser-sync": "^2.23.6",
|
||||||
"browserify": "^16.1.0",
|
"browserify": "^16.1.0",
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 313 KiB |
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 96 96"
|
||||||
|
style="enable-background:new 0 0 96 96;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<metadata
|
||||||
|
id="metadata11"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||||
|
<defs
|
||||||
|
id="defs9" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;stroke-width:0.40677965"
|
||||||
|
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||||
|
id="path3710"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 96 96"
|
||||||
|
style="enable-background:new 0 0 96 96;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<metadata
|
||||||
|
id="metadata11"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||||
|
<defs
|
||||||
|
id="defs9" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke-width:0.40677965"
|
||||||
|
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||||
|
id="path3710"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.65" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 352 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 227 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.85" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 554 B After Width: | Height: | Size: 534 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 534 B |
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 96 96"
|
||||||
|
style="enable-background:new 0 0 96 96;"
|
||||||
|
xml:space="preserve">
|
||||||
|
<metadata
|
||||||
|
id="metadata11"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||||
|
<defs
|
||||||
|
id="defs9" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke-width:0.40677965"
|
||||||
|
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||||
|
id="path3710"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 335 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.65" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 332 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg fill="#000000" height="48" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="none" d="M0 0h20v20H0V0z"/>
|
||||||
|
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 790 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 746 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 0 24 24" width="48">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 195 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 210 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 285 B After Width: | Height: | Size: 266 B |
|
|
@ -1,13 +1,13 @@
|
||||||
[data-component='ChatWidget'] {
|
[data-component='ChatWidget'] {
|
||||||
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0 10px 10px 0;
|
margin: 0 10px 10px 0;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
z-index: 9999;
|
z-index: 100;
|
||||||
|
|
||||||
> .launcher {
|
> .launcher {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.focus {
|
&.focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -36,6 +37,17 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
> .badge{
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.7vmin;
|
||||||
|
top: -1vmin;
|
||||||
|
font-size: 1.5vmin;
|
||||||
|
left: -1vmin;
|
||||||
|
background: rgba(255,0,0,0.9);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,10 +56,13 @@
|
||||||
box-shadow: 0px 2px 10px 1px #000;
|
box-shadow: 0px 2px 10px 1px #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component='Chat'] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
[data-component='MessageList'] {
|
[data-component='MessageList'] {
|
||||||
background-color: rgba(#fff, 0.9);
|
background-color: rgba(#fff, 0.9);
|
||||||
height: 50vh;
|
height: 91vmin;
|
||||||
max-height: 350px;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
border-radius: 5px 5px 0px 0px;
|
border-radius: 5px 5px 0px 0px;
|
||||||
|
|
@ -102,8 +117,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: rgba(#fff, 0.9);
|
background-color: rgba(#fff, 0.9);
|
||||||
height: 35px;
|
height: 6vmin;
|
||||||
padding: 5px;
|
padding: 0.5vmin;
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
|
|
||||||
> .new-message {
|
> .new-message {
|
||||||
|
|
@ -121,4 +136,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
[data-component='FullScreenView'] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
> .controls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 201;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:; row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4vmin;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.2vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: rgba(#000, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
width: 5vmin;
|
||||||
|
height: 5vmin;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
width: 5vmin;
|
||||||
|
height: 5vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incompatible-video {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: rgba(#fff, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
[data-component='FullView'] {
|
||||||
|
position: relative;
|
||||||
|
flex: 100 100 auto;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgba(#2a4b58, 0.9);
|
||||||
|
background-image: url('/resources/images/buddy.svg');
|
||||||
|
background-position: bottom;
|
||||||
|
background-size: auto 85%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
> .info {
|
||||||
|
$backgroundTint = #000;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5
|
||||||
|
top: 0.6vmin;
|
||||||
|
left: 0.6vmin;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> .media {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .box {
|
||||||
|
padding: 0.4vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgba(#000, 0.25);
|
||||||
|
|
||||||
|
> p {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: rgba(#fff, 0.7);
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> video {
|
||||||
|
flex: 100 100 auto;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
user-select: none;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: .15s;
|
||||||
|
background-color: rgba(#000, 0.75);
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .spinner-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(#000, 0.75);
|
||||||
|
|
||||||
|
.react-spinner {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
.react-spinner_bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 20%;
|
||||||
|
height: 7.8%;
|
||||||
|
top: -3.9%;
|
||||||
|
left: -10%;
|
||||||
|
animation: PeerView-spinner 1.2s linear infinite;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: rgba(#fff, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes FullView-spinner {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0.15; }
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,36 @@
|
||||||
[data-component='Me'] {
|
[data-component='Me'] {
|
||||||
|
flex: 100 100 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
flex-direction: row;
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
|
||||||
|
> .view-container {
|
||||||
|
position: relative;
|
||||||
|
width: 20vmin;
|
||||||
|
height: 15vmin;
|
||||||
|
|
||||||
|
&.webcam {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
> .controls {
|
> .controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10
|
z-index: 10;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction:; row;
|
flex-direction:; row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0.4vmin;
|
||||||
|
|
||||||
> .button {
|
> .button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 4px;
|
margin: 0.2vmin;
|
||||||
margin-left: 0;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: 75%;
|
background-size: 75%;
|
||||||
|
|
@ -28,8 +41,8 @@
|
||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
width: 28px;
|
width: 24px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -38,8 +51,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
+mobile() {
|
+mobile() {
|
||||||
width: 26px;
|
width: 22px;
|
||||||
height: 26px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unsupported {
|
&.unsupported {
|
||||||
|
|
@ -61,7 +74,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.off {
|
&.off {
|
||||||
background-image: url('/resources/images/icon_mic_white_off.svg');
|
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
|
||||||
background-color: rgba(#d42241, 0.7);
|
background-color: rgba(#d42241, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +89,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.off {
|
&.off {
|
||||||
background-image: url('/resources/images/icon_webcam_white_on.svg');
|
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unsupported {
|
&.unsupported {
|
||||||
|
|
@ -84,13 +98,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.change-webcam {
|
&.screen {
|
||||||
&.on {
|
&.on {
|
||||||
background-image: url('/resources/images/icon_change_webcam_black.svg');
|
background-image: url('/resources/images/share-screen-black.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unsupported {
|
&.unsupported {
|
||||||
background-image: url('/resources/images/icon_change_webcam_white_unsupported.svg');
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
background-image: url('/resources/images/icon_fullscreen_black.svg');
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[data-component='Notifications'] {
|
[data-component='Notifications'] {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 65px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
[data-component='ParticipantList'] {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .list {
|
||||||
|
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
|
||||||
|
0 4px 20px 0 rgba(0,0,0,0.19);
|
||||||
|
|
||||||
|
> .list-item {
|
||||||
|
padding: 0.5vmin;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component='ListPeer'] {
|
||||||
|
> .controls {
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0.2vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: rgba(#000, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mic {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_mic_black_on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.webcam {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_webcam_black_on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/share-screen-black.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
padding: 8px 16px;
|
||||||
|
float: left;
|
||||||
|
width: auto;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .peer-info {
|
||||||
|
font-size: 1.4vmin;
|
||||||
|
float: left;
|
||||||
|
width: auto;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0.6vmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,15 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
border: 5px solid rgba(#fff, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.screen) {
|
||||||
|
}
|
||||||
|
|
||||||
+mobile() {
|
+mobile() {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -11,39 +20,117 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .indicators {
|
> .view-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.webcam {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10
|
z-index: 10;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction:; row;
|
flex-direction:; row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0.4vmin;
|
||||||
|
|
||||||
> .icon {
|
> .button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 4px;
|
margin: 0.2vmin;
|
||||||
margin-left: 0;
|
border-radius: 2px;
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: 75%;
|
background-size: 75%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
transition-property: opacity;
|
background-color: rgba(#000, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition-property: opacity, background-color;
|
||||||
transition-duration: 0.15s;
|
transition-duration: 0.15s;
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mic-off {
|
+mobile() {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mic {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_mic_black_on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
|
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.webcam-off {
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.webcam {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_webcam_black_on.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
|
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.screen {
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/share-screen-black.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
background-color: rgba(#d42241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unsupported {
|
||||||
|
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
background-image: url('/resources/images/icon_fullscreen_black.svg');
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5
|
z-index: 5
|
||||||
top: 0;
|
top: 0.6vmin;
|
||||||
|
left: 0.6vmin;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -31,8 +31,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
> .box {
|
> .box {
|
||||||
margin: 4px;
|
padding: 0.4vmin;
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: rgba(#000, 0.25);
|
background-color: rgba(#000, 0.25);
|
||||||
|
|
||||||
|
|
@ -55,27 +54,32 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.6vmin;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgba(#000, 0.25);
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
&.is-me {
|
&.is-me {
|
||||||
padding: 10px;
|
padding: 1vmin;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.is-me) {
|
&:not(.is-me) {
|
||||||
padding: 20px;
|
padding: 1vmin;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+mobile() {
|
+mobile() {
|
||||||
&.is-me {
|
&.is-me {
|
||||||
padding: 10px;
|
padding: 1vmin;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.is-me) {
|
&:not(.is-me) {
|
||||||
padding: 10px;
|
padding: 1vmin;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,15 +118,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> .row {
|
> .row {
|
||||||
margin-top: 4px;
|
margin-top: 0.4vmin;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
> .device-icon {
|
> .device-icon {
|
||||||
height: 18px;
|
height: 12px;
|
||||||
width: 18px;
|
width: 12px;
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -190,39 +194,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .minivideo {
|
|
||||||
height: 15%;
|
|
||||||
width: 15%;
|
|
||||||
bottom: 1%;
|
|
||||||
right: 1%;
|
|
||||||
position: absolute;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> video {
|
|
||||||
flex: 100 100 auto;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
user-select: none;
|
|
||||||
transition-property: opacity;
|
|
||||||
transition-duration: .15s;
|
|
||||||
background-color: rgba(#000, 0.75);
|
|
||||||
|
|
||||||
&.is-me {
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
transition-duration: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
filter: blur(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .volume-container {
|
> .volume-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0
|
top: 0
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
[data-component='Room'] {
|
[data-component='Room'] {
|
||||||
position: relative;
|
AppearFadeIn(300ms);
|
||||||
|
|
||||||
|
> .room-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
transition: width 0.3s;
|
||||||
AppearFadeIn(300ms);
|
|
||||||
|
|
||||||
> .state {
|
> .state {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -149,8 +153,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
+desktop() {
|
+desktop() {
|
||||||
height: 200px;
|
|
||||||
width: 235px;
|
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
border: 1px solid rgba(#fff, 0.15);
|
border: 1px solid rgba(#fff, 0.15);
|
||||||
|
|
@ -216,19 +218,19 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.audio-only {
|
&.login {
|
||||||
background-image: url('/resources/images/icon_audio_only_white.svg');
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_login_white.svg');
|
||||||
&.on {
|
|
||||||
background-image: url('/resources/images/icon_audio_only_black.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.restart-ice {
|
&.settings {
|
||||||
background-image: url('/resources/images/icon_restart_ice_white.svg');
|
&.off {
|
||||||
|
background-image: url('/resources/images/icon_settings_white.svg');
|
||||||
|
}
|
||||||
|
|
||||||
&.on {
|
&.on {
|
||||||
background-image: url('/resources/images/icon_restart_ice__black.svg');
|
background-image: url('/resources/images/icon_settings_black.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,12 +252,179 @@
|
||||||
background-image: url('/resources/images/share-screen-extension.svg');
|
background-image: url('/resources/images/share-screen-extension.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.raise-hand {
|
||||||
|
background-image: url('/resources/images/icon-hand-white.svg');
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon-hand-black.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.leave-meeting {
|
&.leave-meeting {
|
||||||
background-image: url('/resources/images/leave-meeting.svg');
|
background-image: url('/resources/images/leave-meeting.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toolarea-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 20%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FFF;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-root {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.3vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-control {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #333;
|
||||||
|
cursor: default;
|
||||||
|
outline: none;
|
||||||
|
padding: 8px 52px 8px 10px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-control:hover {
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-arrow {
|
||||||
|
border-color: #999 transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 5px 0;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
margin-top: -ceil(2.5);
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 14px;
|
||||||
|
width: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-open .Dropdown-arrow {
|
||||||
|
border-color: transparent transparent #999;
|
||||||
|
border-width: 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-menu {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: -1px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-menu .Dropdown-group > .Dropdown-title {
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: rgba(51, 51, 51, 1.2);
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-option {
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: rgba(51, 51, 51, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-option:last-child {
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-option:hover {
|
||||||
|
background-color: #f2f9fc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-option.is-selected {
|
||||||
|
background-color: #f2f9fc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Dropdown-noresults {
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-list {
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
bottom: -1px;
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab--selected {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #aaa;
|
||||||
|
color: black;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab--disabled {
|
||||||
|
color: GrayText;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab:focus {
|
||||||
|
box-shadow: 0 0 5px hsl(208, 99%, 50%);
|
||||||
|
border-color: hsl(208, 99%, 50%);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab:focus:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 5px;
|
||||||
|
left: -4px;
|
||||||
|
right: -4px;
|
||||||
|
bottom: -5px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tabs__tab-panel--selected {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes Room-info-state-connecting {
|
@keyframes Room-info-state-connecting {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
[data-component='ScreenView'] {
|
||||||
|
position: relative;
|
||||||
|
flex: 100 100 auto;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgba(#2a4b58, 0.9);
|
||||||
|
background-image: url('/resources/images/buddy.svg');
|
||||||
|
background-position: bottom;
|
||||||
|
background-size: auto 85%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
> .info {
|
||||||
|
$backgroundTint = #000;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5
|
||||||
|
top: 0.6vmin;
|
||||||
|
left: 0.6vmin;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> .media {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .box {
|
||||||
|
padding: 0.4vmin;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgba(#000, 0.25);
|
||||||
|
|
||||||
|
> p {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
color: rgba(#fff, 0.7);
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> video {
|
||||||
|
flex: 100 100 auto;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
user-select: none;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: .15s;
|
||||||
|
background-color: rgba(#000, 0.75);
|
||||||
|
|
||||||
|
&.is-me {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .spinner-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(#000, 0.75);
|
||||||
|
|
||||||
|
.react-spinner {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
.react-spinner_bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 20%;
|
||||||
|
height: 7.8%;
|
||||||
|
top: -3.9%;
|
||||||
|
left: -10%;
|
||||||
|
animation: PeerView-spinner 1.2s linear infinite;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: rgba(#fff, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ScreenView-spinner {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0.15; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
[data-component='Settings'] {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
[data-component='ToolAreaButton'] {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 101;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 4px 0;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: rgba(#fff, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition-property: opacity, background-color;
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
+desktop() {
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+mobile() {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-color: rgba(#fff, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toolarea-button {
|
||||||
|
background-image: url('/resources/images/icon_tool_area_white.svg');
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background-image: url('/resources/images/icon_tool_area_black.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component='ToolArea'] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> .tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
order: 1;
|
||||||
|
display: block;
|
||||||
|
padding: 1vmin 0 1vmin 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(#000, 0.3);
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background ease 0.2s;
|
||||||
|
text-align: center;
|
||||||
|
width: 33.33%;
|
||||||
|
font-size: 1.3vmin;
|
||||||
|
height: 3vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tab {
|
||||||
|
order: 99;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
padding: 1vmin;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type="radio"]:checked + label {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type="radio"]:checked + label + .tab {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,15 +40,21 @@ body {
|
||||||
@import './components/Peers';
|
@import './components/Peers';
|
||||||
@import './components/Peer';
|
@import './components/Peer';
|
||||||
@import './components/PeerView';
|
@import './components/PeerView';
|
||||||
|
@import './components/ScreenView';
|
||||||
@import './components/Notifications';
|
@import './components/Notifications';
|
||||||
@import './components/Chat';
|
@import './components/Chat';
|
||||||
|
@import './components/Settings';
|
||||||
|
@import './components/ToolArea';
|
||||||
|
@import './components/ParticipantList';
|
||||||
|
@import './components/FullScreenView';
|
||||||
|
@import './components/FullView';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hack to detect in JS the current media query
|
// Hack to detect in JS the current media query
|
||||||
#multiparty-meeting-media-query-detector {
|
#multiparty-meeting-media-query-detector {
|
||||||
position: relative;
|
position: absolute;
|
||||||
z-index: -1000;
|
z-index: -1000;
|
||||||
bottom: 1px;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=multiparty-meeting is a audio / video meeting service running in the browser and powered by webRTC
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/src/multiparty-meeting/server.js
|
||||||
|
Restart=always
|
||||||
|
User=nobody
|
||||||
|
Group=nogroup
|
||||||
|
Environment=PATH=/usr/bin:/usr/local/bin
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
WorkingDirectory=/usr/local/src/multiparty-meeting
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
module.exports =
|
module.exports =
|
||||||
{
|
{
|
||||||
|
// oAuth2 conf
|
||||||
|
oauth2 :
|
||||||
|
{
|
||||||
|
client_id : '',
|
||||||
|
client_secret : '',
|
||||||
|
providerID : '',
|
||||||
|
redirect_uri : 'https://mYDomainName:port/auth-callback',
|
||||||
|
authorization_endpoint : '',
|
||||||
|
userinfo_endpoint : '',
|
||||||
|
token_endpoint : '',
|
||||||
|
scopes : { request : [ 'openid', 'userid','profile'] },
|
||||||
|
response_type : 'code'
|
||||||
|
},
|
||||||
// Listening hostname for `gulp live|open`.
|
// Listening hostname for `gulp live|open`.
|
||||||
domain : 'localhost',
|
domain : 'localhost',
|
||||||
tls :
|
tls :
|
||||||
|
|
@ -7,6 +20,8 @@ module.exports =
|
||||||
cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`,
|
cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`,
|
||||||
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
|
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
|
||||||
},
|
},
|
||||||
|
// Listening port for https server.
|
||||||
|
listeningPort : 3443,
|
||||||
mediasoup :
|
mediasoup :
|
||||||
{
|
{
|
||||||
// mediasoup Server settings.
|
// mediasoup Server settings.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"access-control-allow-origin": "*",
|
||||||
|
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"access-control-allow-headers": "content-type, accept",
|
||||||
|
"access-control-max-age": 10,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.prepareResponse = function(req, cb) {
|
||||||
|
var data = "";
|
||||||
|
req.on('data', function(chunk) { data += chunk; });
|
||||||
|
req.on('end', function() { cb(data); });
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.respond = function(res, data, status) {
|
||||||
|
status = status || 200;
|
||||||
|
res.writeHead(status, headers);
|
||||||
|
res.end(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.send404 = function(res) {
|
||||||
|
exports.respond(res, 'Not Found', 404);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.redirector = function(res, loc, status) {
|
||||||
|
status = status || 302;
|
||||||
|
res.writeHead(status, { Location: loc });
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
|
@ -254,12 +254,32 @@ class Room extends EventEmitter
|
||||||
|
|
||||||
protooPeer.send(
|
protooPeer.send(
|
||||||
'chat-history-receive',
|
'chat-history-receive',
|
||||||
{ chatHistory : this._chatHistory }
|
{ chatHistory: this._chatHistory }
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'raisehand-message':
|
||||||
|
{
|
||||||
|
accept();
|
||||||
|
|
||||||
|
const { raiseHandState } = request.data;
|
||||||
|
const { mediaPeer } = protooPeer.data;
|
||||||
|
|
||||||
|
mediaPeer.appData.raiseHand = request.data.raiseHandState;
|
||||||
|
// Spread to others via protoo.
|
||||||
|
this._protooRoom.spread(
|
||||||
|
'raisehand-message',
|
||||||
|
{
|
||||||
|
peerName : protooPeer.id,
|
||||||
|
raiseHandState : raiseHandState
|
||||||
|
},
|
||||||
|
[ protooPeer ]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
logger.error('unknown request.method "%s"', request.method);
|
logger.error('unknown request.method "%s"', request.method);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"colors": "^1.1.2",
|
"colors": "^1.1.2",
|
||||||
"debug": "^3.1.0",
|
"debug": "^3.1.0",
|
||||||
"express": "^4.16.2",
|
"express": "^4.16.2",
|
||||||
"mediasoup": "^2.0.14",
|
"mediasoup": "^2.1.0",
|
||||||
"protoo-server": "^2.0.7"
|
"protoo-server": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require( 'events' );
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
const path = require('path');
|
||||||
|
const url = require('url');
|
||||||
|
const httpHelpers = require('./http-helpers');
|
||||||
|
const fs = require('fs');
|
||||||
|
const config = require('./config');
|
||||||
|
const utils = require('./util');
|
||||||
|
const querystring = require('querystring');
|
||||||
|
const https = require('https')
|
||||||
|
const Logger = require('./lib/Logger');
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
let authRequests = {}; // ongoing auth requests :
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
state:
|
||||||
|
{
|
||||||
|
peerName:'peerName'
|
||||||
|
code:'oauth2 code',
|
||||||
|
roomId: 'romid',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const actions = {
|
||||||
|
'GET': function(req, res) {
|
||||||
|
var parsedUrl = url.parse(req.url,true);
|
||||||
|
if ( parsedUrl.pathname === '/auth-callback' )
|
||||||
|
{
|
||||||
|
if ( typeof(authRequests[parsedUrl.query.state]) != 'undefined' )
|
||||||
|
{
|
||||||
|
console.log('got authorization code for access token: ',parsedUrl.query,authRequests[parsedUrl.query.state]);
|
||||||
|
const auth = "Basic " + new Buffer(config.oauth2.client_id + ":" + config.oauth2.client_secret).toString("base64");
|
||||||
|
const postUrl = url.parse(config.oauth2.token_endpoint);
|
||||||
|
let postData = querystring.stringify({
|
||||||
|
"grant_type":"authorization_code",
|
||||||
|
"code":parsedUrl.query.code,
|
||||||
|
"redirect_uri":config.oauth2.redirect_uri
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = https.request( {
|
||||||
|
host : postUrl.hostname,
|
||||||
|
path : postUrl.pathname,
|
||||||
|
port : postUrl.port,
|
||||||
|
method : 'POST',
|
||||||
|
headers :
|
||||||
|
{
|
||||||
|
'Content-Type' : 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization' : auth,
|
||||||
|
'Content-Length': Buffer.byteLength(postData)
|
||||||
|
}
|
||||||
|
}, function(res)
|
||||||
|
{
|
||||||
|
res.setEncoding("utf8");
|
||||||
|
let body = "";
|
||||||
|
res.on("data", data => {
|
||||||
|
body += data;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
if ( res.statusCode == 200 )
|
||||||
|
{
|
||||||
|
console.log('We\'ve got an access token!', body);
|
||||||
|
body = JSON.parse(body);
|
||||||
|
authRequests[parsedUrl.query.state].access_token =
|
||||||
|
body.access_token;
|
||||||
|
const auth = "Bearer " + body.access_token;
|
||||||
|
const getUrl = url.parse(config.oauth2.userinfo_endpoint);
|
||||||
|
let request = https.request( {
|
||||||
|
host : getUrl.hostname,
|
||||||
|
path : getUrl.pathname,
|
||||||
|
port : getUrl.port,
|
||||||
|
method : 'GET',
|
||||||
|
headers :
|
||||||
|
{
|
||||||
|
'Authorization' : auth,
|
||||||
|
}
|
||||||
|
}, function(res)
|
||||||
|
{
|
||||||
|
res.setEncoding("utf8");
|
||||||
|
let body = '';
|
||||||
|
res.on("data", data => {
|
||||||
|
body += data;
|
||||||
|
});
|
||||||
|
res.on("end", () => {
|
||||||
|
// we don't need this any longer:
|
||||||
|
delete authRequests[parsedUrl.query.state].access_token;
|
||||||
|
|
||||||
|
body = JSON.parse(body);
|
||||||
|
console.log(body);
|
||||||
|
if ( res.statusCode == 200 )
|
||||||
|
{
|
||||||
|
authRequests[parsedUrl.query.state].verified = true;
|
||||||
|
if ( typeof(body.sub) != 'undefined')
|
||||||
|
{
|
||||||
|
authRequests[parsedUrl.query.state].sub = body.sub;
|
||||||
|
}
|
||||||
|
if ( typeof(body.name) != 'undefined')
|
||||||
|
{
|
||||||
|
authRequests[parsedUrl.query.state].name = body.name;
|
||||||
|
}
|
||||||
|
if ( typeof(body.picture) != 'undefined')
|
||||||
|
{
|
||||||
|
authRequests[parsedUrl.query.state].picture = body.picture;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
authRequests[parsedUrl.query.state].verified = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventEmitter.emit('auth',
|
||||||
|
authRequests[parsedUrl.query.state]);
|
||||||
|
|
||||||
|
delete authRequests[parsedUrl.query.state];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
request.write(' ');
|
||||||
|
request.end;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log('access_token denied',body);
|
||||||
|
authRequests[parsedUrl.query.state].verified = false;
|
||||||
|
delete authRequests[parsedUrl.query.state].access_token;
|
||||||
|
eventEmitter.emit('auth',
|
||||||
|
authRequests[parsedUrl.query.state]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
request.write(postData);
|
||||||
|
request.end;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn('Got authorization_code for unseen state:', parsedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (parsedUrl.pathname === '/login') {
|
||||||
|
const state = utils.random(10);
|
||||||
|
httpHelpers.redirector(res, config.oauth2.authorization_endpoint
|
||||||
|
+ '?client_id=' + config.oauth2.client_id
|
||||||
|
+ '&redirect_uri=' + config.oauth2.redirect_uri
|
||||||
|
+ '&state=' + state
|
||||||
|
+ '&scopes=' + config.oauth2.scopes.request.join('+')
|
||||||
|
+ '&response_type=' + config.oauth2.response_type);
|
||||||
|
authRequests[state] =
|
||||||
|
{
|
||||||
|
'roomId' : parsedUrl.query.roomId,
|
||||||
|
'peerName' : parsedUrl.query.peerName
|
||||||
|
};
|
||||||
|
console.log('Started authorization process: ', parsedUrl.query);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log('requested url:', parsedUrl.pathname);
|
||||||
|
var resolvedBase = path.resolve('./public');
|
||||||
|
var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, '');
|
||||||
|
var fileLoc = path.join(resolvedBase, safeSuffix);
|
||||||
|
|
||||||
|
var stream = fs.createReadStream(fileLoc);
|
||||||
|
|
||||||
|
// Handle non-existent file -> delivering index.html
|
||||||
|
stream.on('error', function(error) {
|
||||||
|
stream = fs.createReadStream(path.resolve('./public/index.html'));
|
||||||
|
res.statusCode = 200;
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// File exists, stream it to user
|
||||||
|
res.statusCode = 200;
|
||||||
|
stream.pipe(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'POST': function(req, res) {
|
||||||
|
httpHelpers.prepareResponse(req, function(data) {
|
||||||
|
// Do something with the data that was just collected by the helper
|
||||||
|
// e.g., validate and save to db
|
||||||
|
// either redirect or respond
|
||||||
|
// should be based on result of the operation performed in response to the POST request intent
|
||||||
|
// e.g., if user wants to save, and save fails, throw error
|
||||||
|
httpHelpers.redirector(res, /* redirect path , optional status code - defaults to 302 */);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = eventEmitter;
|
||||||
|
|
||||||
|
module.exports.handleRequest = function(req, res) {
|
||||||
|
var action = actions[req.method];
|
||||||
|
action ? action(req, res) : httpHelpers.send404(res);
|
||||||
|
};
|
||||||
|
|
@ -14,7 +14,9 @@ console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const router = require('./router');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
const path = require('path');
|
||||||
const protooServer = require('protoo-server');
|
const protooServer = require('protoo-server');
|
||||||
const mediasoup = require('mediasoup');
|
const mediasoup = require('mediasoup');
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
|
|
@ -77,25 +79,34 @@ mediaServer.on('newroom', (room) =>
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTTPS server for the protoo WebSocket server.
|
// HTTPS server
|
||||||
const tls =
|
const tls =
|
||||||
{
|
{
|
||||||
cert : fs.readFileSync(config.tls.cert),
|
cert : fs.readFileSync(config.tls.cert),
|
||||||
key : fs.readFileSync(config.tls.key)
|
key : fs.readFileSync(config.tls.key)
|
||||||
};
|
};
|
||||||
|
|
||||||
const httpsServer = https.createServer(tls, (req, res) =>
|
const httpsServer = https.createServer(tls, router.handleRequest);
|
||||||
|
httpsServer.listen(config.listeningPort, '0.0.0.0', () =>
|
||||||
{
|
{
|
||||||
res.writeHead(404, 'Not Here');
|
logger.info('Server running, port: ',config.listeningPort);
|
||||||
res.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
httpsServer.listen(3443, '0.0.0.0', () =>
|
router.on('auth',function(event){
|
||||||
{
|
console.log('router: Got an event: ',event)
|
||||||
logger.info('protoo WebSocket server running');
|
if ( rooms.has(event.roomId) )
|
||||||
});
|
{
|
||||||
|
const room = rooms.get(event.roomId)._protooRoom;
|
||||||
|
if ( room.hasPeer(event.peerName) )
|
||||||
|
{
|
||||||
|
const peer = room.getPeer(event.peerName);
|
||||||
|
peer.send('auth', event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Protoo WebSocket server.
|
// Protoo WebSocket server listens to same webserver so everythink is available
|
||||||
|
// via same port
|
||||||
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
||||||
{
|
{
|
||||||
maxReceivedFrameSize : 960000, // 960 KBytes.
|
maxReceivedFrameSize : 960000, // 960 KBytes.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
exports.random = function (howMany, chars) {
|
||||||
|
chars = chars
|
||||||
|
|| "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789";
|
||||||
|
var rnd = crypto.randomBytes(howMany)
|
||||||
|
, value = new Array(howMany)
|
||||||
|
, len = len = Math.min(256, chars.length)
|
||||||
|
, d = 256 / len
|
||||||
|
|
||||||
|
for (var i = 0; i < howMany; i++) {
|
||||||
|
value[i] = chars[Math.floor(rnd[i] / d)]
|
||||||
|
};
|
||||||
|
|
||||||
|
return value.join('');
|
||||||
|
}
|
||||||