|
|
@@ -0,0 +1,590 @@
|
|
|
+const mediaType = {
|
|
|
+ audio: 'audioType',
|
|
|
+ video: 'videoType',
|
|
|
+ screen: 'screenType'
|
|
|
+}
|
|
|
+const _EVENTS = {
|
|
|
+ exitRoom: 'exitRoom',
|
|
|
+ openRoom: 'openRoom',
|
|
|
+ startVideo: 'startVideo',
|
|
|
+ stopVideo: 'stopVideo',
|
|
|
+ startAudio: 'startAudio',
|
|
|
+ stopAudio: 'stopAudio',
|
|
|
+ startScreen: 'startScreen',
|
|
|
+ stopScreen: 'stopScreen'
|
|
|
+}
|
|
|
+let producer = null;
|
|
|
+
|
|
|
+class RoomClient {
|
|
|
+
|
|
|
+ constructor(localMediaEl, remoteVideoEl, remoteAudioEl, mediasoupClient, socket, room_id, name, successCallback, that) {
|
|
|
+ this.name = name
|
|
|
+ this.localMediaEl = localMediaEl
|
|
|
+ this.remoteVideoEl = remoteVideoEl
|
|
|
+ this.remoteAudioEl = remoteAudioEl
|
|
|
+ this.mediasoupClient = mediasoupClient
|
|
|
+ this.socket = socket
|
|
|
+ this.producerTransport = null
|
|
|
+ this.consumerTransport = null
|
|
|
+ this.device = null
|
|
|
+ this.room_id = room_id
|
|
|
+
|
|
|
+ this.consumers = new Map()
|
|
|
+ this.producers = new Map()
|
|
|
+ /**
|
|
|
+ * map that contains a mediatype as key and producer_id as value
|
|
|
+ */
|
|
|
+ this.producerLabel = new Map()
|
|
|
+
|
|
|
+ this._isOpen = false
|
|
|
+ this.eventListeners = new Map()
|
|
|
+ Object.keys(_EVENTS).forEach(function (evt) {
|
|
|
+ this.eventListeners.set(evt, [])
|
|
|
+ }.bind(this))
|
|
|
+ this.createRoom(room_id).then(async function () {
|
|
|
+ await this.join(name, room_id)
|
|
|
+ this.initSockets()
|
|
|
+ this._isOpen = true
|
|
|
+ successCallback()
|
|
|
+ }.bind(this))
|
|
|
+ }
|
|
|
+
|
|
|
+ ////////// INIT /////////
|
|
|
+
|
|
|
+ async createRoom(room_id) {
|
|
|
+ await this.socket.request('createRoom', {
|
|
|
+ room_id
|
|
|
+ }).catch(err => {
|
|
|
+ console.log(err)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async join(name, room_id) {
|
|
|
+ this.socket.request('join', {
|
|
|
+ name,
|
|
|
+ room_id
|
|
|
+ }).then(async function (e) {
|
|
|
+ const data = await this.socket.request('getRouterRtpCapabilities');
|
|
|
+ let device = await this.loadDevice(data)
|
|
|
+ this.device = device
|
|
|
+ await this.initTransports(device)
|
|
|
+ this.socket.emit('getProducers')
|
|
|
+ }.bind(this)).catch(e => {
|
|
|
+ console.log(e)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadDevice(routerRtpCapabilities) {
|
|
|
+ let device
|
|
|
+ try {
|
|
|
+ device = new this.mediasoupClient.Device();
|
|
|
+ } catch (error) {
|
|
|
+ if (error.name === 'UnsupportedError') {
|
|
|
+ console.error('browser not supported');
|
|
|
+ }
|
|
|
+ console.error(error)
|
|
|
+ }
|
|
|
+ await device.load({
|
|
|
+ routerRtpCapabilities
|
|
|
+ })
|
|
|
+ return device
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ async initTransports(device) {
|
|
|
+
|
|
|
+ // init producerTransport
|
|
|
+ {
|
|
|
+ const data = await this.socket.request('createWebRtcTransport', {
|
|
|
+ forceTcp: false,
|
|
|
+ rtpCapabilities: device.rtpCapabilities,
|
|
|
+ })
|
|
|
+ if (data.error) {
|
|
|
+ console.error(data.error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.producerTransport = device.createSendTransport(data);
|
|
|
+
|
|
|
+ this.producerTransport.on('connect', async function ({
|
|
|
+ dtlsParameters
|
|
|
+ }, callback, errback) {
|
|
|
+ this.socket.request('connectTransport', {
|
|
|
+ dtlsParameters,
|
|
|
+ transport_id: data.id
|
|
|
+ })
|
|
|
+ .then(callback)
|
|
|
+ .catch(errback)
|
|
|
+ }.bind(this));
|
|
|
+
|
|
|
+ this.producerTransport.on('produce', async function ({
|
|
|
+ kind,
|
|
|
+ rtpParameters
|
|
|
+ }, callback, errback) {
|
|
|
+ try {
|
|
|
+ const {
|
|
|
+ producer_id
|
|
|
+ } = await this.socket.request('produce', {
|
|
|
+ producerTransportId: this.producerTransport.id,
|
|
|
+ kind,
|
|
|
+ rtpParameters,
|
|
|
+ });
|
|
|
+ callback({
|
|
|
+ id: producer_id
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ errback(err);
|
|
|
+ }
|
|
|
+ }.bind(this))
|
|
|
+
|
|
|
+ this.producerTransport.on('connectionstatechange', function (state) {
|
|
|
+ switch (state) {
|
|
|
+ case 'connecting':
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'connected':
|
|
|
+ //localVideo.srcObject = stream
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'failed':
|
|
|
+ this.producerTransport.close();
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }.bind(this));
|
|
|
+ }
|
|
|
+
|
|
|
+ // init consumerTransport
|
|
|
+ {
|
|
|
+ const data = await this.socket.request('createWebRtcTransport', {
|
|
|
+ forceTcp: false,
|
|
|
+ });
|
|
|
+ if (data.error) {
|
|
|
+ console.error(data.error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // only one needed
|
|
|
+ this.consumerTransport = device.createRecvTransport(data);
|
|
|
+ this.consumerTransport.on('connect', function ({
|
|
|
+ dtlsParameters
|
|
|
+ }, callback, errback) {
|
|
|
+ this.socket.request('connectTransport', {
|
|
|
+ transport_id: this.consumerTransport.id,
|
|
|
+ dtlsParameters
|
|
|
+ })
|
|
|
+ .then(callback)
|
|
|
+ .catch(errback);
|
|
|
+ }.bind(this));
|
|
|
+
|
|
|
+ this.consumerTransport.on('connectionstatechange', async function (state) {
|
|
|
+ console.log('state', state);
|
|
|
+ switch (state) {
|
|
|
+ case 'connecting':
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'connected':
|
|
|
+ //remoteVideo.srcObject = await stream;
|
|
|
+ //await socket.request('resume');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'failed':
|
|
|
+ this.consumerTransport.close();
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }.bind(this));
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ initSockets() {
|
|
|
+ this.socket.on('consumerClosed', function ({
|
|
|
+ consumer_id
|
|
|
+ }) {
|
|
|
+ console.log('closing consumer:', consumer_id)
|
|
|
+ this.removeConsumer(consumer_id)
|
|
|
+ }.bind(this))
|
|
|
+
|
|
|
+ /**
|
|
|
+ * data: [ {
|
|
|
+ * producer_id:
|
|
|
+ * producer_socket_id:
|
|
|
+ * }]
|
|
|
+ */
|
|
|
+ console.log('=====producers======');
|
|
|
+ this.socket.on('newProducers', async function (data) {
|
|
|
+ console.log(123);
|
|
|
+ console.log(data);
|
|
|
+ for (let {
|
|
|
+ producer_id
|
|
|
+ } of data) {
|
|
|
+ await this.consume(producer_id)
|
|
|
+ }
|
|
|
+ }.bind(this))
|
|
|
+
|
|
|
+ this.socket.on('disconnect', function () {
|
|
|
+ this.exit(true)
|
|
|
+ }.bind(this))
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //////// MAIN FUNCTIONS /////////////
|
|
|
+
|
|
|
+
|
|
|
+ async produce(type, deviceId = null) {
|
|
|
+ let mediaConstraints = {}
|
|
|
+ let audio = false
|
|
|
+ let screen = false
|
|
|
+ switch (type) {
|
|
|
+ case mediaType.audio:
|
|
|
+ mediaConstraints = {
|
|
|
+ audio: {
|
|
|
+ deviceId: deviceId
|
|
|
+ },
|
|
|
+ video: false
|
|
|
+ }
|
|
|
+ audio = true
|
|
|
+ break
|
|
|
+ case mediaType.video:
|
|
|
+ mediaConstraints = {
|
|
|
+ audio: false,
|
|
|
+ video: {
|
|
|
+ width: {
|
|
|
+ min: 640,
|
|
|
+ ideal: 1920
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ min: 400,
|
|
|
+ ideal: 1080
|
|
|
+ },
|
|
|
+ deviceId: deviceId
|
|
|
+ /*aspectRatio: {
|
|
|
+ ideal: 1.7777777778
|
|
|
+ }*/
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case mediaType.screen:
|
|
|
+ mediaConstraints = false
|
|
|
+ screen = true
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ return
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (!this.device.canProduce('video') && !audio) {
|
|
|
+ console.error('cannot produce video');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.producerLabel.has(type)) {
|
|
|
+ console.log('producer already exists for this type ' + type)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ console.log('mediacontraints:', mediaConstraints)
|
|
|
+ let stream;
|
|
|
+ try {
|
|
|
+ stream = screen ? await navigator.mediaDevices.getDisplayMedia() : await navigator.mediaDevices.getUserMedia(mediaConstraints)
|
|
|
+ console.log(navigator.mediaDevices.getSupportedConstraints())
|
|
|
+
|
|
|
+
|
|
|
+ const track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]
|
|
|
+ const params = {
|
|
|
+ track
|
|
|
+ };
|
|
|
+ if (!audio && !screen) {
|
|
|
+ params.encodings = [{
|
|
|
+ rid: 'r0',
|
|
|
+ maxBitrate: 100000,
|
|
|
+ //scaleResolutionDownBy: 10.0,
|
|
|
+ scalabilityMode: 'S1T3'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ rid: 'r1',
|
|
|
+ maxBitrate: 300000,
|
|
|
+ scalabilityMode: 'S1T3'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ rid: 'r2',
|
|
|
+ maxBitrate: 900000,
|
|
|
+ scalabilityMode: 'S1T3'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ params.codecOptions = {
|
|
|
+ videoGoogleStartBitrate: 1000
|
|
|
+ };
|
|
|
+ }
|
|
|
+ producer = await this.producerTransport.produce(params)
|
|
|
+
|
|
|
+ console.log('producer', producer)
|
|
|
+
|
|
|
+ this.producers.set(producer.id, producer)
|
|
|
+
|
|
|
+ let elem
|
|
|
+ if (!audio) {
|
|
|
+ elem = document.createElement('video')
|
|
|
+ elem.srcObject = stream
|
|
|
+ elem.id = producer.id
|
|
|
+ elem.playsinline = false
|
|
|
+ elem.autoplay = true
|
|
|
+ elem.className = "vid"
|
|
|
+ this.localMediaEl.appendChild(elem)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ producer.on('trackended', () => {
|
|
|
+ this.closeProducer(type)
|
|
|
+ })
|
|
|
+
|
|
|
+ producer.on('transportclose', () => {
|
|
|
+ console.log('producer transport close')
|
|
|
+ if (!audio) {
|
|
|
+ elem.srcObject.getTracks().forEach(function (track) {
|
|
|
+ track.stop()
|
|
|
+ })
|
|
|
+ elem.parentNode.removeChild(elem)
|
|
|
+ }
|
|
|
+ this.producers.delete(producer.id)
|
|
|
+
|
|
|
+ })
|
|
|
+
|
|
|
+ producer.on('close', () => {
|
|
|
+ console.log('closing producer')
|
|
|
+ if (!audio) {
|
|
|
+ elem.srcObject.getTracks().forEach(function (track) {
|
|
|
+ track.stop()
|
|
|
+ })
|
|
|
+ elem.parentNode.removeChild(elem)
|
|
|
+ }
|
|
|
+ this.producers.delete(producer.id)
|
|
|
+
|
|
|
+ })
|
|
|
+
|
|
|
+ this.producerLabel.set(type, producer.id)
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case mediaType.audio:
|
|
|
+ this.event(_EVENTS.startAudio)
|
|
|
+ break
|
|
|
+ case mediaType.video:
|
|
|
+ this.event(_EVENTS.startVideo)
|
|
|
+ break
|
|
|
+ case mediaType.screen:
|
|
|
+ this.event(_EVENTS.startScreen)
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ return
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.log(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async consume(producer_id) {
|
|
|
+ console.log(123);
|
|
|
+ //let info = await roomInfo()
|
|
|
+
|
|
|
+ this.getConsumeStream(producer_id).then(function ({
|
|
|
+ consumer,
|
|
|
+ stream,
|
|
|
+ kind
|
|
|
+ }) {
|
|
|
+
|
|
|
+ this.consumers.set(consumer.id, consumer)
|
|
|
+ let elem;
|
|
|
+ if (kind === 'video') {
|
|
|
+ console.log(stream);
|
|
|
+ elem = document.createElement('video')
|
|
|
+ elem.srcObject = stream
|
|
|
+ elem.id = consumer.id
|
|
|
+ elem.playsinline = false
|
|
|
+ elem.autoplay = true
|
|
|
+ elem.className = "vid"
|
|
|
+ this.remoteVideoEl.appendChild(elem)
|
|
|
+ } else {
|
|
|
+ elem = document.createElement('audio')
|
|
|
+ elem.srcObject = stream
|
|
|
+ elem.id = consumer.id
|
|
|
+ elem.playsinline = false
|
|
|
+ elem.autoplay = true
|
|
|
+ this.remoteAudioEl.appendChild(elem)
|
|
|
+ }
|
|
|
+
|
|
|
+ consumer.on('trackended', function () {
|
|
|
+ this.removeConsumer(consumer.id)
|
|
|
+ }.bind(this))
|
|
|
+ consumer.on('transportclose', function () {
|
|
|
+ this.removeConsumer(consumer.id)
|
|
|
+ }.bind(this))
|
|
|
+
|
|
|
+
|
|
|
+ }.bind(this))
|
|
|
+ }
|
|
|
+
|
|
|
+ async getConsumeStream(producerId) {
|
|
|
+ const {
|
|
|
+ rtpCapabilities
|
|
|
+ } = this.device
|
|
|
+ const data = await this.socket.request('consume', {
|
|
|
+ rtpCapabilities,
|
|
|
+ consumerTransportId: this.consumerTransport.id, // might be
|
|
|
+ producerId
|
|
|
+ });
|
|
|
+ const {
|
|
|
+ id,
|
|
|
+ kind,
|
|
|
+ rtpParameters,
|
|
|
+ } = data;
|
|
|
+
|
|
|
+ let codecOptions = {};
|
|
|
+ const consumer = await this.consumerTransport.consume({
|
|
|
+ id,
|
|
|
+ producerId,
|
|
|
+ kind,
|
|
|
+ rtpParameters,
|
|
|
+ codecOptions,
|
|
|
+ })
|
|
|
+ const stream = new MediaStream();
|
|
|
+ stream.addTrack(consumer.track);
|
|
|
+ return {
|
|
|
+ consumer,
|
|
|
+ stream,
|
|
|
+ kind
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ closeProducer(type) {
|
|
|
+ if (!this.producerLabel.has(type)) {
|
|
|
+ console.log('there is no producer for this type ' + type)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let producer_id = this.producerLabel.get(type)
|
|
|
+ console.log(producer_id)
|
|
|
+ this.socket.emit('producerClosed', {
|
|
|
+ producer_id
|
|
|
+ })
|
|
|
+ this.producers.get(producer_id).close()
|
|
|
+ this.producers.delete(producer_id)
|
|
|
+ this.producerLabel.delete(type)
|
|
|
+
|
|
|
+ if (type !== mediaType.audio) {
|
|
|
+ let elem = document.getElementById(producer_id)
|
|
|
+ elem.srcObject.getTracks().forEach(function (track) {
|
|
|
+ track.stop()
|
|
|
+ })
|
|
|
+ elem.parentNode.removeChild(elem)
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case mediaType.audio:
|
|
|
+ this.event(_EVENTS.stopAudio)
|
|
|
+ break
|
|
|
+ case mediaType.video:
|
|
|
+ this.event(_EVENTS.stopVideo)
|
|
|
+ break
|
|
|
+ case mediaType.screen:
|
|
|
+ this.event(_EVENTS.stopScreen)
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ return
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ pauseProducer(type) {
|
|
|
+ if (!this.producerLabel.has(type)) {
|
|
|
+ console.log('there is no producer for this type ' + type)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let producer_id = this.producerLabel.get(type)
|
|
|
+ this.producers.get(producer_id).pause()
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ resumeProducer(type) {
|
|
|
+ if (!this.producerLabel.has(type)) {
|
|
|
+ console.log('there is no producer for this type ' + type)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let producer_id = this.producerLabel.get(type)
|
|
|
+ this.producers.get(producer_id).resume()
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ removeConsumer(consumer_id) {
|
|
|
+ let elem = document.getElementById(consumer_id)
|
|
|
+ elem.srcObject.getTracks().forEach(function (track) {
|
|
|
+ track.stop()
|
|
|
+ })
|
|
|
+ elem.parentNode.removeChild(elem)
|
|
|
+
|
|
|
+ this.consumers.delete(consumer_id)
|
|
|
+ }
|
|
|
+
|
|
|
+ exit(offline = false) {
|
|
|
+
|
|
|
+ let clean = function () {
|
|
|
+ this._isOpen = false
|
|
|
+ this.consumerTransport.close()
|
|
|
+ this.producerTransport.close()
|
|
|
+ this.socket.off('disconnect')
|
|
|
+ this.socket.off('newProducers')
|
|
|
+ this.socket.off('consumerClosed')
|
|
|
+ }.bind(this)
|
|
|
+
|
|
|
+ if (!offline) {
|
|
|
+ this.socket.request('exitRoom').then(e => console.log(e)).catch(e => console.warn(e)).finally(function () {
|
|
|
+ clean()
|
|
|
+ }.bind(this))
|
|
|
+ } else {
|
|
|
+ clean()
|
|
|
+ }
|
|
|
+
|
|
|
+ this.event(_EVENTS.exitRoom)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /////// HELPERS //////////
|
|
|
+
|
|
|
+ async roomInfo() {
|
|
|
+ let info = await socket.request('getMyRoomInfo')
|
|
|
+ return info
|
|
|
+ }
|
|
|
+
|
|
|
+ static get mediaType() {
|
|
|
+ return mediaType
|
|
|
+ }
|
|
|
+
|
|
|
+ event(evt) {
|
|
|
+ if (this.eventListeners.has(evt)) {
|
|
|
+ this.eventListeners.get(evt).forEach(callback => callback())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ on(evt, callback) {
|
|
|
+ console.log('evt', evt);
|
|
|
+ console.log('callback', callback);
|
|
|
+ this.eventListeners.get(evt).push(callback)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //////// GETTERS ////////
|
|
|
+
|
|
|
+ isOpen() {
|
|
|
+ return this._isOpen
|
|
|
+ }
|
|
|
+
|
|
|
+ static get EVENTS() {
|
|
|
+ return _EVENTS
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = RoomClient;
|