Sfoglia il codice sorgente

Project HPS, first pass

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
dev1
Philipp Hancke 5 anni fa
parent
commit
7f090de2c9

+ 58
- 0
JitsiConference.js Vedi File

@@ -27,6 +27,7 @@ import IceFailedNotification
27 27
     from './modules/connectivity/IceFailedNotification';
28 28
 import ParticipantConnectionStatusHandler
29 29
     from './modules/connectivity/ParticipantConnectionStatus';
30
+import E2EEContext from './modules/e2ee/E2EEContext';
30 31
 import E2ePing from './modules/e2eping/e2eping';
31 32
 import Jvb121EventGenerator from './modules/event/Jvb121EventGenerator';
32 33
 import RecordingManager from './modules/recording/RecordingManager';
@@ -233,6 +234,10 @@ export default function JitsiConference(options) {
233 234
     this.videoSIPGWHandler = new VideoSIPGW(this.room);
234 235
     this.recordingManager = new RecordingManager(this.room);
235 236
     this._conferenceJoinAnalyticsEventSent = false;
237
+
238
+    if (browser.supportsInsertableStreams()) {
239
+        this._e2eeCtx = new E2EEContext({ salt: this.options.name });
240
+    }
236 241
 }
237 242
 
238 243
 // FIXME convert JitsiConference to ES6 - ASAP !
@@ -1108,6 +1113,18 @@ JitsiConference.prototype._setupNewTrack = function(newTrack) {
1108 1113
     newTrack._setConference(this);
1109 1114
 
1110 1115
     this.eventEmitter.emit(JitsiConferenceEvents.TRACK_ADDED, newTrack);
1116
+
1117
+    // Setup E2EE handling, if supported.
1118
+    if (this._e2eeCtx) {
1119
+        const activeTPC = this.getActivePeerConnection();
1120
+        const sender = activeTPC ? activeTPC.findSenderForTrack(newTrack.track) : null;
1121
+
1122
+        if (sender) {
1123
+            this._e2eeCtx.handleSender(sender, newTrack.getType());
1124
+        } else {
1125
+            logger.warn(`Could not handle E2EE for local ${newTrack.getType()} track: sender not found`);
1126
+        }
1127
+    }
1111 1128
 };
1112 1129
 
1113 1130
 /**
@@ -1636,6 +1653,18 @@ JitsiConference.prototype.onRemoteTrackAdded = function(track) {
1636 1653
         return;
1637 1654
     }
1638 1655
 
1656
+    // Setup E2EE handling, if supported.
1657
+    if (this._e2eeCtx) {
1658
+        const activeTPC = this.getActivePeerConnection();
1659
+        const receiver = activeTPC ? activeTPC.findReceiverForTrack(track.track) : null;
1660
+
1661
+        if (receiver) {
1662
+            this._e2eeCtx.handleReceiver(receiver, track.getType());
1663
+        } else {
1664
+            logger.warn(`Could not handle E2EE for remote ${track.getType()} track: receiver not found`);
1665
+        }
1666
+    }
1667
+
1639 1668
     const id = track.getParticipantId();
1640 1669
     const participant = this.getParticipantById(id);
1641 1670
 
@@ -1710,6 +1739,8 @@ JitsiConference.prototype.onTransportInfo = function(session, transportInfo) {
1710 1739
  * @param {JitsiRemoteTrack} removedTrack
1711 1740
  */
1712 1741
 JitsiConference.prototype.onRemoteTrackRemoved = function(removedTrack) {
1742
+    // TODO: handle E2EE.
1743
+
1713 1744
     this.getParticipants().forEach(participant => {
1714 1745
         const tracks = participant.getTracks();
1715 1746
 
@@ -1837,6 +1868,7 @@ JitsiConference.prototype._acceptJvbIncomingCall = function(
1837 1868
             p2p: false,
1838 1869
             value: now
1839 1870
         }));
1871
+
1840 1872
     try {
1841 1873
         jingleSession.initialize(this.room, this.rtc, this.options.config);
1842 1874
     } catch (error) {
@@ -3269,3 +3301,29 @@ JitsiConference.prototype._sendConferenceJoinAnalyticsEvent = function() {
3269 3301
     }));
3270 3302
     this._conferenceJoinAnalyticsEventSent = true;
3271 3303
 };
3304
+
3305
+/**
3306
+ * Returns whether End-To-End encryption is supported. Note that not all participants
3307
+ * in the conference may support it.
3308
+ *
3309
+ * @returns {boolean}
3310
+ */
3311
+JitsiConference.prototype.isE2EESupported = function() {
3312
+    return Boolean(this._e2eeCtx);
3313
+};
3314
+
3315
+/**
3316
+ * Sets the key to be used for End-To-End encryption.
3317
+ *
3318
+ * @param {string} key the key to be used.
3319
+ * @returns {void}
3320
+ */
3321
+JitsiConference.prototype.setE2EEKey = function(key) {
3322
+    if (!this._e2eeCtx) {
3323
+        logger.warn('Cannot set E2EE key: there is no defined context, platform is likely unsupported.');
3324
+
3325
+        return;
3326
+    }
3327
+
3328
+    this._e2eeCtx.setKey(key);
3329
+};

+ 7
- 0
modules/RTC/RTC.js Vedi File

@@ -505,6 +505,13 @@ export default class RTC extends Listenable {
505 505
         }
506 506
 
507 507
         // FIXME: We should rename iceConfig to pcConfig.
508
+
509
+        if (!isP2P && browser.supportsInsertableStreams()) {
510
+            logger.debug('E2EE - setting insertable streams constraints');
511
+            iceConfig.forceEncodedAudioInsertableStreams = true;
512
+            iceConfig.forceEncodedVideoInsertableStreams = true;
513
+        }
514
+
508 515
         if (browser.supportsSdpSemantics()) {
509 516
             iceConfig.sdpSemantics = 'plan-b';
510 517
         }

+ 22
- 0
modules/RTC/TraceablePeerConnection.js Vedi File

@@ -1624,6 +1624,28 @@ TraceablePeerConnection.prototype.findSenderByStream = function(stream) {
1624 1624
     return this.peerconnection.getSenders().find(s => s.track === track);
1625 1625
 };
1626 1626
 
1627
+/**
1628
+ * Returns the receiver corresponding to the given MediaStreamTrack.
1629
+ *
1630
+ * @param {MediaSreamTrack} track - The media stream track used for the search.
1631
+ * @returns {RTCRtpReceiver|undefined} - The found receiver or undefined if no receiver
1632
+ * was found.
1633
+ */
1634
+TraceablePeerConnection.prototype.findReceiverForTrack = function(track) {
1635
+    return this.peerconnection.getReceivers().find(r => r.track === track);
1636
+};
1637
+
1638
+/**
1639
+ * Returns the sender corresponding to the given MediaStreamTrack.
1640
+ *
1641
+ * @param {MediaSreamTrack} track - The media stream track used for the search.
1642
+ * @returns {RTCRtpSender|undefined} - The found sender or undefined if no sender
1643
+ * was found.
1644
+ */
1645
+TraceablePeerConnection.prototype.findSenderForTrack = function(track) {
1646
+    return this.peerconnection.getSenders().find(s => s.track === track);
1647
+};
1648
+
1627 1649
 /**
1628 1650
  * Replaces <tt>oldTrack</tt> with <tt>newTrack</tt> from the peer connection.
1629 1651
  * Either <tt>oldTrack</tt> or <tt>newTrack</tt> can be null; replacing a valid

+ 9
- 0
modules/browser/BrowserCapabilities.js Vedi File

@@ -292,6 +292,15 @@ export default class BrowserCapabilities extends BrowserDetection {
292 292
                     !== 'undefined');
293 293
     }
294 294
 
295
+    /**
296
+     * Checks if the browser supports insertable streams, needed for E2EE.
297
+     * @returns {boolean} {@code true} if the browser supports insertable streams.
298
+     */
299
+    supportsInsertableStreams() {
300
+        return Boolean(typeof window.RTCRtpSender !== 'undefined'
301
+            && window.RTCRtpSender.prototype.createEncodedVideoStreams);
302
+    }
303
+
295 304
     /**
296 305
      * Checks if the browser supports the "sdpSemantics" configuration option.
297 306
      * https://webrtc.org/web-apis/chrome/unified-plan/

+ 328
- 0
modules/e2ee/E2EEContext.js Vedi File

@@ -0,0 +1,328 @@
1
+/* global __filename, TransformStream */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+
5
+const logger = getLogger(__filename);
6
+
7
+// We use a ringbuffer of keys so we can change them and still decode packets that were
8
+// encrypted with an old key.
9
+// In the future when we dont rely on a globally shared key we will actually use it. For
10
+// now set the size to 1 which means there is only a single key. This causes some
11
+// glitches when changing the key but its ok.
12
+const keyRingSize = 1;
13
+
14
+// We use a 96 bit IV for AES GCM. This is signalled in plain together with the
15
+// packet. See https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
16
+const ivLength = 12;
17
+
18
+// We copy the first bytes of the VP8 payload unencrypted.
19
+// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
20
+//   https://tools.ietf.org/html/rfc6386#section-9.1
21
+// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
22
+// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
23
+// instead of being unable to decode).
24
+// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
25
+//
26
+// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
27
+//   https://tools.ietf.org/html/rfc6716#section-3.1
28
+const unencryptedBytes = {
29
+    key: 10,
30
+    delta: 3,
31
+    undefined: 1 // frame.type is not set on audio
32
+};
33
+
34
+
35
+/**
36
+ * Context encapsulating the cryptography bits required for E2EE.
37
+ * This uses the WebRTC Insertable Streams API which is explained in
38
+ *   https://github.com/alvestrand/webrtc-media-streams/blob/master/explainer.md
39
+ * that provides access to the encoded frames and allows them to be transformed.
40
+ *
41
+ * The encoded frame format is explained below in the _encodeFunction method.
42
+ * High level design goals were:.
43
+ * - do not require changes to existing SFUs and retain (VP8) metadata.
44
+ * - allow the SFU to rewrite SSRCs, timestamp, pictureId.
45
+ * - allow for the key to be rotated frequently.
46
+ */
47
+export default class E2EEcontext {
48
+
49
+    /**
50
+     * Build a new E2EE context instance, which will be used in a given conference.
51
+     *
52
+     * @param {string} options.salt - Salt to be used for key deviation.
53
+     *      FIXME: We currently use the MUC room name for this which has the same lifetime
54
+     *      as this context. While not (pseudo)random as recommended in
55
+     *        https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params
56
+     *      this is easily available and the same for all participants.
57
+     *      We currently do not enforce a minimum length of 16 bytes either.
58
+     */
59
+    constructor(options) {
60
+        this._options = options;
61
+
62
+        // An array (ring) of keys that we use for sending and receiving.
63
+        this._cryptoKeyRing = new Array(keyRingSize);
64
+
65
+        // A pointer to the currently used key.
66
+        this._currentKeyIndex = -1;
67
+
68
+        // We keep track of how many frames we have sent per ssrc.
69
+        // Starts with a random offset similar to the RTP sequence number.
70
+        this._sendCounts = new Map();
71
+
72
+        // Initialize the salt and convert it once.
73
+        const encoder = new TextEncoder();
74
+
75
+        this._salt = encoder.encode(options.salt);
76
+    }
77
+
78
+    /**
79
+     * Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will injecct
80
+     * a frame decoder.
81
+     *
82
+     * @param {RTCRtpReceiver} receiver - The receiver which will get the decoding function injected.
83
+     * @param {string} kind - The kind of track this receiver belongs to.
84
+     */
85
+    handleReceiver(receiver, kind) {
86
+        const receiverStreams
87
+            = kind === 'video' ? receiver.createEncodedVideoStreams() : receiver.createEncodedAudioStreams();
88
+        const transform = new TransformStream({
89
+            transform: this._decodeFunction.bind(this)
90
+        });
91
+
92
+        receiverStreams.readableStream
93
+            .pipeThrough(transform)
94
+            .pipeTo(receiverStreams.writableStream);
95
+    }
96
+
97
+    /**
98
+     * Handles the given {@code RTCRtpSender} by creating a {@code TransformStream} which will injecct
99
+     * a frame encoder.
100
+     *
101
+     * @param {RTCRtpSender} sender - The sender which will get the encoding funcction injected.
102
+     * @param {string} kind - The kind of track this sender belongs to.
103
+     */
104
+    handleSender(sender, kind) {
105
+        const senderStreams
106
+            = kind === 'video' ? sender.createEncodedVideoStreams() : sender.createEncodedAudioStreams();
107
+        const transform = new TransformStream({
108
+            transform: this._encodeFunction.bind(this)
109
+        });
110
+
111
+        senderStreams.readableStream
112
+            .pipeThrough(transform)
113
+            .pipeTo(senderStreams.writableStream);
114
+    }
115
+
116
+    /**
117
+     * Sets the key to be used for E2EE.
118
+     *
119
+     * @param {string} value - Value to be used as the new key. May be falsy to disable end-to-end encryption.
120
+     */
121
+    async setKey(value) {
122
+        let key;
123
+
124
+        if (value) {
125
+            const encoder = new TextEncoder();
126
+
127
+            key = await this._deriveKey(encoder.encode(value));
128
+        } else {
129
+            key = false;
130
+        }
131
+        this._currentKeyIndex++;
132
+        this._cryptoKeyRing[this._currentKeyIndex % this._cryptoKeyRing.length] = key;
133
+    }
134
+
135
+    /**
136
+     * Derives a AES-GCM key with 128 bits from the input using PBKDF2
137
+     * The salt is configured in the constructor of this class.
138
+     * @param {Uint8Array} keyBytes - Value to derive key from
139
+     */
140
+    async _deriveKey(keyBytes) {
141
+        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
142
+        const material = await crypto.subtle.importKey('raw', keyBytes,
143
+            'PBKDF2', false, [ 'deriveBits', 'deriveKey' ]);
144
+
145
+        // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#PBKDF2
146
+        return crypto.subtle.deriveKey({
147
+            name: 'PBKDF2',
148
+            salt: this._salt,
149
+            iterations: 100000,
150
+            hash: 'SHA-256'
151
+        }, material, {
152
+            name: 'AES-GCM',
153
+            length: 128
154
+        }, false, [ 'encrypt', 'decrypt' ]);
155
+    }
156
+
157
+    /**
158
+     * Construct the IV used for AES-GCM and sent (in plain) with the packet similar to
159
+     * https://tools.ietf.org/html/rfc7714#section-8.1
160
+     * It concatenates
161
+     * - the 32 bit synchronization source (SSRC) given on the encoded frame,
162
+     * - the 32 bit rtp timestamp given on the encoded frame,
163
+     * - a send counter that is specific to the SSRC. Starts at a random number.
164
+     * The send counter is essentially the pictureId but we currently have to implement this ourselves.
165
+     * There is no XOR with a salt. Note that this IV leaks the SSRC to the receiver but since this is
166
+     * randomly generated and SFUs may not rewrite this is considered acceptable.
167
+     * The SSRC is used to allow demultiplexing multiple streams with the same key, as described in
168
+     *   https://tools.ietf.org/html/rfc3711#section-4.1.1
169
+     * The RTP timestamp is 32 bits and advances by the codec clock rate (90khz for video, 48khz for
170
+     * opus audio) every second. For video it rolls over roughly every 13 hours.
171
+     * The send counter will advance at the frame rate (30fps for video, 50fps for 20ms opus audio)
172
+     * every second. It will take a long time to roll over.
173
+     *
174
+     * See also https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
175
+     */
176
+    _makeIV(synchronizationSource, timestamp) {
177
+        const iv = new ArrayBuffer(ivLength);
178
+        const ivView = new DataView(iv);
179
+
180
+        // having to keep our own send count (similar to a picture id) is not ideal.
181
+        if (!this._sendCounts.has(synchronizationSource)) {
182
+            // Initialize with a random offset, similar to the RTP sequence number.
183
+            this._sendCounts.set(synchronizationSource, Math.floor(Math.random() * 0xFFFF));
184
+        }
185
+        const sendCount = this._sendCounts.get(synchronizationSource);
186
+
187
+        ivView.setUint32(0, synchronizationSource);
188
+        ivView.setUint32(4, timestamp);
189
+        ivView.setUint32(8, sendCount % 0xFFFF);
190
+
191
+        this._sendCounts.set(synchronizationSource, sendCount + 1);
192
+
193
+        return iv;
194
+    }
195
+
196
+    /**
197
+     * Function that will be injected in a stream and will encrypt the given encoded frames.
198
+     *
199
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
200
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
201
+     *
202
+     * The packet format is described below. One of the design goals was to not require
203
+     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
204
+     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
205
+     * solve this eventually). This also "hides" that a client is using E2EE a bit.
206
+     *
207
+     * Note that this operates on the full frame, i.e. for VP8 the data described in
208
+     *   https://tools.ietf.org/html/rfc6386#section-9.1
209
+     *
210
+     * The VP8 payload descriptor described in
211
+     *   https://tools.ietf.org/html/rfc7741#section-4.2
212
+     * is part of the RTP packet and not part of the frame and is not controllable by us.
213
+     * This is fine as the SFU keeps having access to it for routing.
214
+     *
215
+     * The encrypted frame is formed as follows:
216
+     * 1) Leave the first (10, 3, 1) bytes unencrypted, depending on the frame type and kind.
217
+     * 2) Form the GCM IV for the frame as described above.
218
+     * 3) Encrypt the rest of the frame using AES-GCM.
219
+     * 4) Allocate space for the encrypted frame.
220
+     * 5) Copy the unencrypted bytes to the start of the encrypted frame.
221
+     * 6) Append the ciphertext to the encrypted frame.
222
+     * 7) Append the IV.
223
+     * 8) Append a single byte for the key identifier. TODO: we don't need all the bits.
224
+     * 9) Enqueue the encrypted frame for sending.
225
+     */
226
+    _encodeFunction(encodedFrame, controller) {
227
+        const keyIndex = this._currentKeyIndex % this._cryptoKeyRing.length;
228
+
229
+        if (this._cryptoKeyRing[keyIndex]) {
230
+            const iv = this._makeIV(encodedFrame.synchronizationSource, encodedFrame.timestamp);
231
+
232
+            return crypto.subtle.encrypt({
233
+                name: 'AES-GCM',
234
+                iv,
235
+                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])
236
+            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, unencryptedBytes[encodedFrame.type]))
237
+            .then(cipherText => {
238
+                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + cipherText.byteLength
239
+                    + iv.byteLength + 1);
240
+                const newUint8 = new Uint8Array(newData);
241
+
242
+                newUint8.set(
243
+                    new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type])); // copy first bytes.
244
+                newUint8.set(
245
+                    new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
246
+                newUint8.set(
247
+                    new Uint8Array(iv), unencryptedBytes[encodedFrame.type] + cipherText.byteLength); // append IV.
248
+                newUint8[unencryptedBytes[encodedFrame.type] + cipherText.byteLength + ivLength]
249
+                    = keyIndex; // set key index.
250
+
251
+                encodedFrame.data = newData;
252
+
253
+                return controller.enqueue(encodedFrame);
254
+            }, e => {
255
+                logger.error(e);
256
+
257
+                // We are not enqueuing the frame here on purpose.
258
+            });
259
+        }
260
+
261
+        /* NOTE WELL:
262
+         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
263
+         * This is ok for demo purposes but should not be done once this becomes more relied upon.
264
+         */
265
+        controller.enqueue(encodedFrame);
266
+    }
267
+
268
+    /**
269
+     * Function that will be injected in a stream and will decrypt the given encoded frames.
270
+     *
271
+     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
272
+     * @param {TransformStreamDefaultController} controller - TransportStreamController.
273
+     *
274
+     * The decrypted frame is formed as follows:
275
+     * 1) Extract the key index from the last byte of the encrypted frame.
276
+     *    If there is no key associated with the key index, the frame is enqueued for decoding
277
+     *    and these steps terminate.
278
+     * 2) Determine the frame type in order to look up the number of unencrypted header bytes.
279
+     * 2) Extract the 12-byte IV from its position near the end of the packet.
280
+     *    Note: the IV is treated as opaque and not reconstructed from the input.
281
+     * 3) Decrypt the encrypted frame content after the unencrypted bytes using AES-GCM.
282
+     * 4) Allocate space for the decrypted frame.
283
+     * 5) Copy the unencrypted bytes from the start of the encrypted frame.
284
+     * 6) Append the plaintext to the decrypted frame.
285
+     * 7) Enqueue the decrypted frame for decoding.
286
+     */
287
+    _decodeFunction(encodedFrame, controller) {
288
+        const data = new Uint8Array(encodedFrame.data);
289
+        const keyIndex = data[encodedFrame.data.byteLength - 1];
290
+
291
+        if (this._cryptoKeyRing[keyIndex]) {
292
+            // TODO: use encodedFrame.type again, see https://bugs.chromium.org/p/chromium/issues/detail?id=1068468
293
+            const encodedFrameType = encodedFrame.type
294
+                ? (data[0] & 0x1) === 0 ? 'key' : 'delta' // eslint-disable-line no-bitwise
295
+                : undefined;
296
+            const iv = new Uint8Array(encodedFrame.data, encodedFrame.data.byteLength - ivLength - 1, ivLength);
297
+            const cipherTextStart = unencryptedBytes[encodedFrameType];
298
+            const cipherTextLength = encodedFrame.data.byteLength - (unencryptedBytes[encodedFrameType] + ivLength + 1);
299
+
300
+            return crypto.subtle.decrypt({
301
+                name: 'AES-GCM',
302
+                iv,
303
+                additionalData: new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrameType])
304
+            }, this._cryptoKeyRing[keyIndex], new Uint8Array(encodedFrame.data, cipherTextStart, cipherTextLength))
305
+            .then(plainText => {
306
+                const newData = new ArrayBuffer(unencryptedBytes[encodedFrameType] + plainText.byteLength);
307
+                const newUint8 = new Uint8Array(newData);
308
+
309
+                newUint8.set(new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrameType]));
310
+                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrameType]);
311
+
312
+                encodedFrame.data = newData;
313
+
314
+                return controller.enqueue(encodedFrame);
315
+            }, e => {
316
+                logger.error(e);
317
+
318
+                // Just feed the (potentially encrypted) frame in case of error.
319
+                // Worst case it is garbage.
320
+                controller.enqueue(encodedFrame);
321
+            });
322
+        }
323
+
324
+        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
325
+        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
326
+        controller.enqueue(encodedFrame);
327
+    }
328
+}

+ 2
- 2
package-lock.json Vedi File

@@ -8785,8 +8785,8 @@
8785 8785
       "integrity": "sha512-T9pJFzn1ZUqZ/we9+OvI5pFdrjeb4IBMbEjK+ZWEZV036wEl8l8GOtF8AJ3sIqOMtdIiFLdFu99JiGWd7yapAQ=="
8786 8786
     },
8787 8787
     "strophejs-plugin-stream-management": {
8788
-      "version": "github:jitsi/strophejs-plugin-stream-management#cec7608601c1bc098543823fc658e3ddf758c009",
8789
-      "from": "github:jitsi/strophejs-plugin-stream-management#cec7608601c1bc098543823fc658e3ddf758c009"
8788
+      "version": "github:jitsi/strophejs-plugin-stream-management#e719a02b4f83856c1530882084a4b048ee622d45",
8789
+      "from": "github:jitsi/strophejs-plugin-stream-management#e719a02b4f83856c1530882084a4b048ee622d45"
8790 8790
     },
8791 8791
     "supports-color": {
8792 8792
       "version": "2.0.0",

+ 13
- 1
webpack.config.js Vedi File

@@ -38,7 +38,19 @@ const config = {
38 38
 
39 39
                         // Tell babel to avoid compiling imports into CommonJS
40 40
                         // so that webpack may do tree shaking.
41
-                        { modules: false }
41
+                        {
42
+                            modules: false,
43
+
44
+                            // Specify our target browsers so no transpiling is
45
+                            // done unnecessarily. For browsers not specified
46
+                            // here, the ES2015+ profile will be used.
47
+                            targets: {
48
+                                chrome: 58,
49
+                                electron: 2,
50
+                                firefox: 54,
51
+                                safari: 11
52
+                            }
53
+                        }
42 54
                     ],
43 55
                     '@babel/preset-flow'
44 56
                 ],

Loading…
Annulla
Salva