Pārlūkot izejas kodu

Add pinch zoom functionality

j8
Zoltan Bettenbuk 7 gadus atpakaļ
vecāks
revīzija
79b7e1641d

+ 12
- 0
react/features/base/media/actionTypes.js Parādīt failu

@@ -49,6 +49,18 @@ export const SET_VIDEO_AVAILABLE = Symbol('SET_VIDEO_AVAILABLE');
49 49
  */
50 50
 export const SET_VIDEO_MUTED = Symbol('SET_VIDEO_MUTED');
51 51
 
52
+/**
53
+ * The type of (redux) action to store the last video {@link Transform} applied
54
+ * to a stream.
55
+ *
56
+ * {
57
+ *     type: STORE_VIDEO_TRANSFORM,
58
+ *     streamId: string,
59
+ *     transform: Transform
60
+ * }
61
+ */
62
+export const STORE_VIDEO_TRANSFORM = Symbol('STORE_VIDEO_TRANSFORM');
63
+
52 64
 /**
53 65
  * The type of (redux) action to toggle the local video camera facing mode. In
54 66
  * contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally

+ 24
- 3
react/features/base/media/actions.js Parādīt failu

@@ -8,6 +8,7 @@ import {
8 8
     SET_CAMERA_FACING_MODE,
9 9
     SET_VIDEO_AVAILABLE,
10 10
     SET_VIDEO_MUTED,
11
+    STORE_VIDEO_TRANSFORM,
11 12
     TOGGLE_CAMERA_FACING_MODE
12 13
 } from './actionTypes';
13 14
 import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
@@ -18,9 +19,9 @@ import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
18 19
  * @param {boolean} available - True if the local audio is to be marked as
19 20
  * available or false if the local audio is not available.
20 21
  * @returns {{
21
- *      type: SET_AUDIO_AVAILABLE,
22
- *      available: boolean
23
- *  }}
22
+ *     type: SET_AUDIO_AVAILABLE,
23
+ *     available: boolean
24
+ * }}
24 25
  */
25 26
 export function setAudioAvailable(available: boolean) {
26 27
     return {
@@ -112,6 +113,26 @@ export function setVideoMuted(
112 113
     };
113 114
 }
114 115
 
116
+/**
117
+ * Creates an action to store the last video {@link Transform} applied to a
118
+ * stream.
119
+ *
120
+ * @param {string} streamId - The ID of the stream.
121
+ * @param {Object} transform - The {@code Transform} to store.
122
+ * @returns {{
123
+ *     type: STORE_VIDEO_TRANSFORM,
124
+ *     streamId: string,
125
+ *     transform: Object
126
+ * }}
127
+ */
128
+export function storeVideoTransform(streamId: string, transform: Object) {
129
+    return {
130
+        type: STORE_VIDEO_TRANSFORM,
131
+        streamId,
132
+        transform
133
+    };
134
+}
135
+
115 136
 /**
116 137
  * Toggles the camera facing mode. Most commonly, for example, mobile devices
117 138
  * such as phones have a front/user-facing and a back/environment-facing

+ 17
- 4
react/features/base/media/components/AbstractVideoTrack.js Parādīt failu

@@ -36,7 +36,12 @@ export default class AbstractVideoTrack extends Component {
36 36
          * of all Videos. For more details, refer to the zOrder property of the
37 37
          * Video class for React Native.
38 38
          */
39
-        zOrder: PropTypes.number
39
+        zOrder: PropTypes.number,
40
+
41
+        /**
42
+         * Indicates whether zooming (pinch to zoom and/or drag) is enabled.
43
+         */
44
+        zoomEnabled: PropTypes.bool
40 45
     };
41 46
 
42 47
     /**
@@ -80,7 +85,7 @@ export default class AbstractVideoTrack extends Component {
80 85
      * @returns {ReactElement}
81 86
      */
82 87
     render() {
83
-        const videoTrack = this.state.videoTrack;
88
+        const { videoTrack } = this.state;
84 89
         let render;
85 90
 
86 91
         if (this.props.waitForVideoStarted) {
@@ -108,13 +113,21 @@ export default class AbstractVideoTrack extends Component {
108 113
         const stream
109 114
             = render ? videoTrack.jitsiTrack.getOriginalStream() : null;
110 115
 
116
+        // Actual zoom is currently only enabled if the stream is a desktop
117
+        // stream.
118
+        const zoomEnabled
119
+            = this.props.zoomEnabled
120
+                && stream
121
+                && videoTrack.videoType === 'desktop';
122
+
111 123
         return (
112 124
             <Video
113 125
                 mirror = { videoTrack && videoTrack.mirror }
114 126
                 onPlaying = { this._onVideoPlaying }
115 127
                 onPress = { this.props.onPress }
116 128
                 stream = { stream }
117
-                zOrder = { this.props.zOrder } />
129
+                zOrder = { this.props.zOrder }
130
+                zoomEnabled = { zoomEnabled } />
118 131
         );
119 132
     }
120 133
 
@@ -125,7 +138,7 @@ export default class AbstractVideoTrack extends Component {
125 138
      * @returns {void}
126 139
      */
127 140
     _onVideoPlaying() {
128
-        const videoTrack = this.props.videoTrack;
141
+        const { videoTrack } = this.props;
129 142
 
130 143
         if (videoTrack && !videoTrack.videoStarted) {
131 144
             this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack));

+ 18
- 14
react/features/base/media/components/native/Video.js Parādīt failu

@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
4 4
 import React, { Component } from 'react';
5 5
 import { RTCView } from 'react-native-webrtc';
6 6
 
7
-import { Pressable } from '../../../react';
8
-
9 7
 import styles from './styles';
8
+import VideoTransform from './VideoTransform';
10 9
 
11 10
 /**
12 11
  * The React Native {@link Component} which is similar to Web's
@@ -54,7 +53,12 @@ export default class Video extends Component<*> {
54 53
          * values: 0 for the remote video(s) which appear in the background, and
55 54
          * 1 for the local video(s) which appear above the remote video(s).
56 55
          */
57
-        zOrder: PropTypes.number
56
+        zOrder: PropTypes.number,
57
+
58
+        /**
59
+         * Indicates whether zooming (pinch to zoom and/or drag) is enabled.
60
+         */
61
+        zoomEnabled: PropTypes.bool
58 62
     };
59 63
 
60 64
     /**
@@ -77,29 +81,29 @@ export default class Video extends Component<*> {
77 81
      * @returns {ReactElement|null}
78 82
      */
79 83
     render() {
80
-        const { stream } = this.props;
84
+        const { stream, zoomEnabled } = this.props;
81 85
 
82 86
         if (stream) {
83 87
             const streamURL = stream.toURL();
84
-
85
-            // XXX The CSS style object-fit that we utilize on Web is not
86
-            // supported on React Native. Adding objectFit to React Native's
87
-            // StyleSheet appears to be impossible without hacking and an
88
-            // unjustified amount of effort. Consequently, I've chosen to define
89
-            // objectFit on RTCView itself. Anyway, prepare to accommodate a
90
-            // future definition of objectFit in React Native's StyleSheet.
91 88
             const style = styles.video;
92
-            const objectFit = (style && style.objectFit) || 'cover';
89
+            const objectFit
90
+                = zoomEnabled
91
+                    ? 'contain'
92
+                    : (style && style.objectFit) || 'cover';
93 93
 
94 94
             return (
95
-                <Pressable onPress = { this.props.onPress }>
95
+                <VideoTransform
96
+                    enabled = { zoomEnabled }
97
+                    onPress = { this.props.onPress }
98
+                    streamId = { stream.id }
99
+                    style = { style }>
96 100
                     <RTCView
97 101
                         mirror = { this.props.mirror }
98 102
                         objectFit = { objectFit }
99 103
                         streamURL = { streamURL }
100 104
                         style = { style }
101 105
                         zOrder = { this.props.zOrder } />
102
-                </Pressable>
106
+                </VideoTransform>
103 107
             );
104 108
         }
105 109
 

+ 714
- 0
react/features/base/media/components/native/VideoTransform.js Parādīt failu

@@ -0,0 +1,714 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { PanResponder, View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { storeVideoTransform } from '../../actions';
8
+import styles from './styles';
9
+
10
+/**
11
+ * The default/initial transform (= no transform).
12
+ */
13
+const DEFAULT_TRANSFORM = {
14
+    scale: 1,
15
+    translateX: 0,
16
+    translateY: 0
17
+};
18
+
19
+/**
20
+ * The minimum scale (magnification) multiplier. 1 is equal to objectFit
21
+ * = 'contain'.
22
+ */
23
+const MIN_SCALE = 1;
24
+
25
+/*
26
+ * The max distance from the edge of the screen where we let the user move the
27
+ * view to. This is large enough now to let the user drag the view to a position
28
+ * where no other displayed components cover it (such as filmstrip). If a
29
+ * ViewPort (hint) support is added to the LargeVideo component then this
30
+ * contant will not be necessary anymore.
31
+ */
32
+const MAX_OFFSET = 100;
33
+
34
+/**
35
+ * The max allowed scale (magnification) multiplier.
36
+ */
37
+const MAX_SCALE = 5;
38
+
39
+/**
40
+ * The length of a minimum movement after which we consider a gesture a move
41
+ * instead of a tap/long tap.
42
+ */
43
+const MOVE_THRESHOLD_DISMISSES_TOUCH = 2;
44
+
45
+/**
46
+ * A tap timeout after which we consider a gesture a long tap and will not
47
+ * trigger onPress (unless long tap gesture support is added in the future).
48
+ */
49
+const TAP_TIMEOUT_MS = 400;
50
+
51
+/**
52
+ * Type of a transform object this component is capable of handling.
53
+ */
54
+type Transform = {
55
+    scale: number,
56
+    translateX: number,
57
+    translateY: number
58
+};
59
+
60
+type Props = {
61
+
62
+    /**
63
+     * The children components of this view.
64
+     */
65
+    children: Object,
66
+
67
+    /**
68
+     * Transformation is only enabled when this flag is true.
69
+     */
70
+    enabled: boolean,
71
+
72
+    /**
73
+     * Function to invoke when a press event is detected.
74
+     */
75
+    onPress?: Function,
76
+
77
+    /**
78
+     * The id of the current stream that is displayed.
79
+     */
80
+    streamId: string,
81
+
82
+    /**
83
+     * Style of the top level transformable view.
84
+     */
85
+    style: Object,
86
+
87
+    /**
88
+     * The stored transforms retreived from Redux to be initially applied
89
+     * to different streams.
90
+     */
91
+    _transforms: Object,
92
+
93
+    /**
94
+     * Action to dispatch when the component is unmounted.
95
+     */
96
+    _onUnmount: Function
97
+};
98
+
99
+type State = {
100
+
101
+    /**
102
+     * The current (non-transformed) layout of the View.
103
+     */
104
+    layout: ?Object,
105
+
106
+    /**
107
+     * The current transform that is applied.
108
+     */
109
+    transform: Transform
110
+};
111
+
112
+/**
113
+ * An container that captures gestures such as pinch&zoom, touch or move.
114
+ */
115
+class VideoTransform extends Component<Props, State> {
116
+    /**
117
+     * The gesture handler object.
118
+     */
119
+    gestureHandlers: PanResponder;
120
+
121
+    /**
122
+     * The initial distance of the fingers on pinch start.
123
+     */
124
+    initialDistance: number;
125
+
126
+    /**
127
+     * The initial position of the finger on touch start.
128
+     */
129
+    initialPosition: {
130
+        x: number,
131
+        y: number
132
+    };
133
+
134
+    /**
135
+     * Time of the last tap.
136
+     */
137
+    lastTap: number;
138
+
139
+    /**
140
+     * Constructor of the component.
141
+     *
142
+     * @inheritdoc
143
+     */
144
+    constructor(props: Props) {
145
+        super(props);
146
+
147
+        this.state = {
148
+            layout: null,
149
+            transform: DEFAULT_TRANSFORM
150
+        };
151
+
152
+        this._getTransformStyle = this._getTransformStyle.bind(this);
153
+        this._onGesture = this._onGesture.bind(this);
154
+        this._onLayout = this._onLayout.bind(this);
155
+        this._onMoveShouldSetPanResponder
156
+            = this._onMoveShouldSetPanResponder.bind(this);
157
+        this._onPanResponderGrant = this._onPanResponderGrant.bind(this);
158
+        this._onPanResponderMove = this._onPanResponderMove.bind(this);
159
+        this._onPanResponderRelease = this._onPanResponderRelease.bind(this);
160
+        this._onStartShouldSetPanResponder
161
+            = this._onStartShouldSetPanResponder.bind(this);
162
+    }
163
+
164
+    /**
165
+     * Implements React Component's componentWillMount.
166
+     *
167
+     * @inheritdoc
168
+     */
169
+    componentWillMount() {
170
+        this.gestureHandlers = PanResponder.create({
171
+            onPanResponderGrant: this._onPanResponderGrant,
172
+            onPanResponderMove: this._onPanResponderMove,
173
+            onPanResponderRelease: this._onPanResponderRelease,
174
+            onPanResponderTerminationRequest: () => true,
175
+            onMoveShouldSetPanResponder: this._onMoveShouldSetPanResponder,
176
+            onShouldBlockNativeResponder: () => false,
177
+            onStartShouldSetPanResponder: this._onStartShouldSetPanResponder
178
+        });
179
+
180
+        const { streamId } = this.props;
181
+
182
+        this._restoreTransform(streamId);
183
+    }
184
+
185
+    /**
186
+     * Implements React Component's componentWillReceiveProps.
187
+     *
188
+     * @inheritdoc
189
+     */
190
+    componentWillReceiveProps({ streamId: newStreamId }) {
191
+        if (this.props.streamId !== newStreamId) {
192
+            this._storeTransform();
193
+            this._restoreTransform(newStreamId);
194
+        }
195
+    }
196
+
197
+    /**
198
+     * Implements React Component's componentWillUnmount.
199
+     *
200
+     * @inheritdoc
201
+     */
202
+    componentWillUnmount() {
203
+        this._storeTransform();
204
+    }
205
+
206
+    /**
207
+     * Renders the empty component that captures the gestures.
208
+     *
209
+     * @inheritdoc
210
+     */
211
+    render() {
212
+        const { children, style } = this.props;
213
+
214
+        return (
215
+            <View
216
+                onLayout = { this._onLayout }
217
+                pointerEvents = 'box-only'
218
+                style = { [
219
+                    styles.videoTransformedViewContaier,
220
+                    style
221
+                ] }
222
+                { ...this.gestureHandlers.panHandlers }>
223
+                <View
224
+                    style = { [
225
+                        styles.videoTranformedView,
226
+                        this._getTransformStyle()
227
+                    ] }>
228
+                    { children }
229
+                </View>
230
+            </View>
231
+        );
232
+    }
233
+
234
+    /**
235
+     * Calculates the new transformation to be applied by merging the current
236
+     * transform values with the newly received incremental values.
237
+     *
238
+     * @param {Transform} transform - The new transform object.
239
+     * @private
240
+     * @returns {Transform}
241
+     */
242
+    _calculateTransformIncrement(transform: Transform) {
243
+        let {
244
+            scale,
245
+            translateX,
246
+            translateY
247
+        } = this.state.transform;
248
+        const {
249
+            scale: newScale,
250
+            translateX: newTranslateX,
251
+            translateY: newTranslateY
252
+        } = transform;
253
+
254
+        // Note: We don't limit MIN_SCALE here yet, as we need to detect a scale
255
+        // down gesture even if the scale is already at MIN_SCALE to let the
256
+        // user return the screen to center with that gesture. Scale is limited
257
+        // to MIN_SCALE right before it gets applied.
258
+        scale = Math.min(scale * (newScale || 1), MAX_SCALE);
259
+
260
+        translateX = translateX + ((newTranslateX || 0) / scale);
261
+        translateY = translateY + ((newTranslateY || 0) / scale);
262
+
263
+        return {
264
+            scale,
265
+            translateX,
266
+            translateY
267
+        };
268
+    }
269
+
270
+    _didMove: Object => boolean
271
+
272
+    /**
273
+     * Determines if there was large enough movement to be handled.
274
+     *
275
+     * @param {Object} gestureState - The gesture state.
276
+     * @returns {boolean}
277
+     */
278
+    _didMove({ dx, dy }) {
279
+        return Math.abs(dx) > MOVE_THRESHOLD_DISMISSES_TOUCH
280
+                || Math.abs(dy) > MOVE_THRESHOLD_DISMISSES_TOUCH;
281
+    }
282
+
283
+    _getTouchDistance: Object => number;
284
+
285
+    /**
286
+     * Calculates the touch distance on a pinch event.
287
+     *
288
+     * @param {Object} evt - The touch event.
289
+     * @private
290
+     * @returns {number}
291
+     */
292
+    _getTouchDistance({ nativeEvent: { touches } }) {
293
+        const dx = Math.abs(touches[0].pageX - touches[1].pageX);
294
+        const dy = Math.abs(touches[0].pageY - touches[1].pageY);
295
+
296
+        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
297
+    }
298
+
299
+    _getTouchPosition: Object => Object
300
+
301
+    /**
302
+     * Calculates the position of the touch event.
303
+     *
304
+     * @param {Object} evt - The touch event.
305
+     * @private
306
+     * @returns {Object}
307
+     */
308
+    _getTouchPosition({ nativeEvent: { touches } }) {
309
+        return {
310
+            x: touches[0].pageX,
311
+            y: touches[0].pageY
312
+        };
313
+    }
314
+
315
+    _getTransformStyle: () => Object
316
+
317
+    /**
318
+     * Generates a transform style object to be used on the component.
319
+     *
320
+     * @returns {{string: Array<{string: number}>}}
321
+     */
322
+    _getTransformStyle() {
323
+        const { enabled } = this.props;
324
+
325
+        if (!enabled) {
326
+            return null;
327
+        }
328
+
329
+        const {
330
+            scale,
331
+            translateX,
332
+            translateY
333
+        } = this.state.transform;
334
+
335
+        return {
336
+            transform: [
337
+                { scale },
338
+                { translateX },
339
+                { translateY }
340
+            ]
341
+        };
342
+    }
343
+
344
+    /**
345
+     * Limits the move matrix and then applies the transformation to the
346
+     * component (updates state).
347
+     *
348
+     * Note: Points A (top-left) and D (bottom-right) are opposite points of
349
+     * the View rectangle.
350
+     *
351
+     * @param {Transform} transform - The transformation object.
352
+     * @private
353
+     * @returns {void}
354
+     */
355
+    _limitAndApplyTransformation(transform: Transform) {
356
+        const { layout } = this.state;
357
+
358
+        if (layout) {
359
+            const { scale } = this.state.transform;
360
+            const { scale: newScaleUnlimited } = transform;
361
+            let {
362
+                translateX: newTranslateX,
363
+                translateY: newTranslateY
364
+            } = transform;
365
+
366
+            // Scale is only limited to MIN_SCALE here to detect downscale
367
+            // gesture later.
368
+            const newScale = Math.max(newScaleUnlimited, MIN_SCALE);
369
+
370
+            // The A and D points of the original View (before transform).
371
+            const originalLayout = {
372
+                a: {
373
+                    x: layout.x,
374
+                    y: layout.y
375
+                },
376
+                d: {
377
+                    x: layout.x + layout.width,
378
+                    y: layout.y + layout.height
379
+                }
380
+            };
381
+
382
+            // The center point (midpoint) of the transformed View.
383
+            const transformedCenterPoint = {
384
+                x: ((layout.x + layout.width) / 2) + (newTranslateX * newScale),
385
+                y: ((layout.y + layout.height) / 2) + (newTranslateY * newScale)
386
+            };
387
+
388
+            // The size of the transformed View.
389
+            const transformedSize = {
390
+                height: layout.height * newScale,
391
+                width: layout.width * newScale
392
+            };
393
+
394
+            // The A and D points of the transformed View.
395
+            const transformedLayout = {
396
+                a: {
397
+                    x: transformedCenterPoint.x - (transformedSize.width / 2),
398
+                    y: transformedCenterPoint.y - (transformedSize.height / 2)
399
+                },
400
+                d: {
401
+                    x: transformedCenterPoint.x + (transformedSize.width / 2),
402
+                    y: transformedCenterPoint.y + (transformedSize.height / 2)
403
+                }
404
+            };
405
+
406
+            let _MAX_OFFSET = MAX_OFFSET;
407
+
408
+            if (newScaleUnlimited < scale) {
409
+                // This is a negative scale event so we dynamycally reduce the
410
+                // MAX_OFFSET to get the screen back to the center on
411
+                // downscaling.
412
+                _MAX_OFFSET = Math.min(MAX_OFFSET, MAX_OFFSET * (newScale - 1));
413
+            }
414
+
415
+            // Correct move matrix if it goes out of the view
416
+            // too much (_MAX_OFFSET).
417
+            newTranslateX
418
+                -= Math.max(
419
+                    transformedLayout.a.x - originalLayout.a.x - _MAX_OFFSET,
420
+                    0);
421
+            newTranslateX
422
+                += Math.max(
423
+                    originalLayout.d.x - transformedLayout.d.x - _MAX_OFFSET,
424
+                    0);
425
+            newTranslateY
426
+                -= Math.max(
427
+                    transformedLayout.a.y - originalLayout.a.y - _MAX_OFFSET,
428
+                    0);
429
+            newTranslateY
430
+                += Math.max(
431
+                    originalLayout.d.y - transformedLayout.d.y - _MAX_OFFSET,
432
+                    0);
433
+
434
+            this.setState({
435
+                transform: {
436
+                    scale: newScale,
437
+                    translateX: Math.round(newTranslateX),
438
+                    translateY: Math.round(newTranslateY)
439
+                }
440
+            });
441
+        }
442
+    }
443
+
444
+    _onGesture: (string, ?Object | number) => void
445
+
446
+    /**
447
+     * Handles gestures and converts them to transforms.
448
+     *
449
+     * Currently supported gestures:
450
+     *  - scale (punch&zoom-type scale).
451
+     *  - move
452
+     *  - press.
453
+     *
454
+     * Note: This component supports onPress solely to overcome the problem of
455
+     * not being able to register gestures via the PanResponder due to the fact
456
+     * that the entire Conference component was a single touch responder
457
+     * component in the past (see base/react/.../Container with an onPress
458
+     * event) - and stock touch responder components seem to have exclusive
459
+     * priority in handling touches in React.
460
+     *
461
+     * @param {string} type - The type of the gesture.
462
+     * @param {?Object | number} value - The value of the gesture, if any.
463
+     * @returns {void}
464
+     */
465
+    _onGesture(type, value) {
466
+        let transform;
467
+
468
+        switch (type) {
469
+        case 'move':
470
+            transform = {
471
+                ...DEFAULT_TRANSFORM,
472
+                translateX: value.x,
473
+                translateY: value.y
474
+            };
475
+            break;
476
+        case 'scale':
477
+            transform = {
478
+                ...DEFAULT_TRANSFORM,
479
+                scale: value
480
+            };
481
+            break;
482
+
483
+        case 'press': {
484
+            const { onPress } = this.props;
485
+
486
+            typeof onPress === 'function' && onPress();
487
+            break;
488
+        }
489
+        }
490
+
491
+        if (transform) {
492
+            this._limitAndApplyTransformation(
493
+                this._calculateTransformIncrement(transform));
494
+        }
495
+
496
+        this.lastTap = 0;
497
+    }
498
+
499
+    _onLayout: Object => void
500
+
501
+    /**
502
+     * Callback for the onLayout of the component.
503
+     *
504
+     * @param {Object} event - The native props of the onLayout event.
505
+     * @private
506
+     * @returns {void}
507
+     */
508
+    _onLayout({ nativeEvent: { layout: { x, y, width, height } } }) {
509
+        this.setState({
510
+            layout: {
511
+                x,
512
+                y,
513
+                width,
514
+                height
515
+            }
516
+        });
517
+    }
518
+
519
+    _onMoveShouldSetPanResponder: (Object, Object) => boolean
520
+
521
+    /**
522
+     * Function to decide whether the responder should respond to a move event.
523
+     *
524
+     * @param {Object} evt - The event.
525
+     * @param {Object} gestureState - Gesture state.
526
+     * @private
527
+     * @returns {boolean}
528
+     */
529
+    _onMoveShouldSetPanResponder(evt, gestureState) {
530
+        return this.props.enabled
531
+            && (this._didMove(gestureState)
532
+                || gestureState.numberActiveTouches === 2);
533
+    }
534
+
535
+    _onPanResponderGrant: (Object, Object) => void
536
+
537
+    /**
538
+     * Calculates the initial touch distance.
539
+     *
540
+     * @param {Object} evt - Touch event.
541
+     * @param {Object} gestureState - Gesture state.
542
+     * @private
543
+     * @returns {void}
544
+     */
545
+    _onPanResponderGrant(evt, { numberActiveTouches }) {
546
+        if (numberActiveTouches === 1) {
547
+            this.initialPosition = this._getTouchPosition(evt);
548
+            this.lastTap = Date.now();
549
+
550
+        } else if (numberActiveTouches === 2) {
551
+            this.initialDistance = this._getTouchDistance(evt);
552
+        }
553
+    }
554
+
555
+    _onPanResponderMove: (Object, Object) => void
556
+
557
+    /**
558
+     * Handles the PanResponder move (touch move) event.
559
+     *
560
+     * @param {Object} evt - Touch event.
561
+     * @param {Object} gestureState - Gesture state.
562
+     * @private
563
+     * @returns {void}
564
+     */
565
+    _onPanResponderMove(evt, gestureState) {
566
+        if (gestureState.numberActiveTouches === 2) {
567
+            // this is a zoom event
568
+            if (
569
+                this.initialDistance === undefined
570
+                || isNaN(this.initialDistance)
571
+            ) {
572
+                // there is no initial distance because the user started
573
+                // with only one finger. We calculate it now.
574
+                this.initialDistance = this._getTouchDistance(evt);
575
+            } else {
576
+                const distance = this._getTouchDistance(evt);
577
+                const scale = distance / (this.initialDistance || 1);
578
+
579
+                this.initialDistance = distance;
580
+
581
+                this._onGesture('scale', scale);
582
+            }
583
+        } else if (gestureState.numberActiveTouches === 1
584
+                && isNaN(this.initialDistance)
585
+                && this._didMove(gestureState)) {
586
+            // this is a move event
587
+            const position = this._getTouchPosition(evt);
588
+            const move = {
589
+                x: position.x - this.initialPosition.x,
590
+                y: position.y - this.initialPosition.y
591
+            };
592
+
593
+            this.initialPosition = position;
594
+
595
+            this._onGesture('move', move);
596
+        }
597
+    }
598
+
599
+    _onPanResponderRelease: () => void
600
+
601
+    /**
602
+     * Handles the PanResponder gesture end event.
603
+     *
604
+     * @private
605
+     * @returns {void}
606
+     */
607
+    _onPanResponderRelease() {
608
+        if (this.lastTap && Date.now() - this.lastTap < TAP_TIMEOUT_MS) {
609
+            this._onGesture('press');
610
+        }
611
+        delete this.initialDistance;
612
+        delete this.initialPosition;
613
+    }
614
+
615
+    _onStartShouldSetPanResponder: () => boolean
616
+
617
+    /**
618
+     * Function to decide whether the responder should respond to a start
619
+     * (thouch) event.
620
+     *
621
+     * @private
622
+     * @returns {boolean}
623
+     */
624
+    _onStartShouldSetPanResponder() {
625
+        return typeof this.props.onPress === 'function';
626
+    }
627
+
628
+    /**
629
+     * Restores the last applied transform when the component is mounted, or
630
+     * a new stream is about to be rendered.
631
+     *
632
+     * @param {string} streamId - The stream id to restore transform for.
633
+     * @private
634
+     * @returns {void}
635
+     */
636
+    _restoreTransform(streamId) {
637
+        const { enabled, _transforms } = this.props;
638
+
639
+        if (enabled) {
640
+            const initialTransform = _transforms[streamId];
641
+
642
+            if (initialTransform) {
643
+                this.setState({
644
+                    transform: initialTransform
645
+                });
646
+            }
647
+        }
648
+    }
649
+
650
+    /**
651
+     * Stores/saves the current transform when the component is destroyed, or a
652
+     * new stream is about to be rendered.
653
+     *
654
+     * @private
655
+     * @returns {void}
656
+     */
657
+    _storeTransform() {
658
+        const { _onUnmount, enabled, streamId } = this.props;
659
+
660
+        if (enabled) {
661
+            _onUnmount(streamId, this.state.transform);
662
+        }
663
+    }
664
+}
665
+
666
+/**
667
+ * Maps dispatching of some action to React component props.
668
+ *
669
+ * @param {Function} dispatch - Redux action dispatcher.
670
+ * @private
671
+ * @returns {{
672
+ *     _onUnmount: Function
673
+ * }}
674
+ */
675
+function _mapDispatchToProps(dispatch) {
676
+    return {
677
+        /**
678
+         * Dispatches actions to store the last applied transform to a video.
679
+         *
680
+         * @param {string} streamId - The ID of the stream.
681
+         * @param {Transform} transform - The last applied transform.
682
+         * @private
683
+         * @returns {void}
684
+         */
685
+        _onUnmount(streamId, transform) {
686
+            dispatch(storeVideoTransform(streamId, transform));
687
+        }
688
+    };
689
+}
690
+
691
+/**
692
+ * Maps (parts of) the redux state to the component's props.
693
+ *
694
+ * @param {Object} state - The redux state.
695
+ * @param {Object} ownProps - The component's own props.
696
+ * @private
697
+ * @returns {{
698
+ *     _transforms: Object
699
+ * }}
700
+ */
701
+function _mapStateToProps(state) {
702
+    return {
703
+        /**
704
+         * The stored transforms retreived from Redux to be initially applied to
705
+         * different streams.
706
+         *
707
+         * @private
708
+         * @type {Object}
709
+         */
710
+        _transforms: state['features/base/media'].video.transforms
711
+    };
712
+}
713
+
714
+export default connect(_mapStateToProps, _mapDispatchToProps)(VideoTransform);

+ 17
- 0
react/features/base/media/components/native/styles.js Parādīt failu

@@ -6,6 +6,23 @@ import { ColorPalette } from '../../../styles';
6 6
  * The styles of the feature base/media.
7 7
  */
8 8
 export default StyleSheet.create({
9
+
10
+    /**
11
+     * Base style of the transformed video view.
12
+     */
13
+    videoTranformedView: {
14
+        flex: 1
15
+    },
16
+
17
+    /**
18
+     * A basic style to avoid rendering a transformed view off the component,
19
+     * that can be visible on special occasions, such as during device rotate
20
+     * animation, or PiP mode.
21
+     */
22
+    videoTransformedViewContaier: {
23
+        overflow: 'hidden'
24
+    },
25
+
9 26
     /**
10 27
      * Make {@code Video} fill its container.
11 28
      */

+ 82
- 1
react/features/base/media/reducer.js Parādīt failu

@@ -1,6 +1,8 @@
1 1
 import { combineReducers } from 'redux';
2 2
 
3
+import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference';
3 4
 import { ReducerRegistry } from '../redux';
5
+import { TRACK_REMOVED } from '../tracks';
4 6
 
5 7
 import {
6 8
     SET_AUDIO_AVAILABLE,
@@ -8,6 +10,7 @@ import {
8 10
     SET_CAMERA_FACING_MODE,
9 11
     SET_VIDEO_AVAILABLE,
10 12
     SET_VIDEO_MUTED,
13
+    STORE_VIDEO_TRANSFORM,
11 14
     TOGGLE_CAMERA_FACING_MODE
12 15
 } from './actionTypes';
13 16
 import { CAMERA_FACING_MODE } from './constants';
@@ -73,7 +76,13 @@ function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
73 76
 const VIDEO_INITIAL_MEDIA_STATE = {
74 77
     available: true,
75 78
     facingMode: CAMERA_FACING_MODE.USER,
76
-    muted: 0
79
+    muted: 0,
80
+
81
+    /**
82
+     * The video {@link Transform}s applied to {@code MediaStream}s by
83
+     * {@code id} i.e. "pinch to zoom".
84
+     */
85
+    transforms: {}
77 86
 };
78 87
 
79 88
 /**
@@ -87,6 +96,10 @@ const VIDEO_INITIAL_MEDIA_STATE = {
87 96
  */
88 97
 function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
89 98
     switch (action.type) {
99
+    case CONFERENCE_FAILED:
100
+    case CONFERENCE_LEFT:
101
+        return _clearAllVideoTransforms(state);
102
+
90 103
     case SET_CAMERA_FACING_MODE:
91 104
         return {
92 105
             ...state,
@@ -105,6 +118,9 @@ function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
105 118
             muted: action.muted
106 119
         };
107 120
 
121
+    case STORE_VIDEO_TRANSFORM:
122
+        return _storeVideoTransform(state, action);
123
+
108 124
     case TOGGLE_CAMERA_FACING_MODE: {
109 125
         let cameraFacingMode = state.facingMode;
110 126
 
@@ -119,6 +135,9 @@ function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
119 135
         };
120 136
     }
121 137
 
138
+    case TRACK_REMOVED:
139
+        return _trackRemoved(state, action);
140
+
122 141
     default:
123 142
         return state;
124 143
     }
@@ -138,3 +157,65 @@ ReducerRegistry.register('features/base/media', combineReducers({
138 157
     audio: _audio,
139 158
     video: _video
140 159
 }));
160
+
161
+/**
162
+ * Removes all stored video {@link Transform}s.
163
+ *
164
+ * @param {Object} state - The {@code video} state of the feature base/media.
165
+ * @private
166
+ * @returns {Object}
167
+ */
168
+function _clearAllVideoTransforms(state) {
169
+    return {
170
+        ...state,
171
+        transforms: VIDEO_INITIAL_MEDIA_STATE.transforms
172
+    };
173
+}
174
+
175
+/**
176
+ * Stores the last applied transform to a stream.
177
+ *
178
+ * @param {Object} state - The {@code video} state of the feature base/media.
179
+ * @param {Object} action - The redux action {@link STORE_VIDEO_TRANSFORM}.
180
+ * @private
181
+ * @returns {Object}
182
+ */
183
+function _storeVideoTransform(state, { streamId, transform }) {
184
+    return {
185
+        ...state,
186
+        transforms: {
187
+            ...state.transforms,
188
+            [streamId]: transform
189
+        }
190
+    };
191
+}
192
+
193
+/**
194
+ * Removes the stored video {@link Transform} associated with a
195
+ * {@code MediaStream} when its respective track is removed.
196
+ *
197
+ * @param {Object} state - The {@code video} state of the feature base/media.
198
+ * @param {Object} action - The redux action {@link TRACK_REMOVED}.
199
+ * @private
200
+ * @returns {Object}
201
+ */
202
+function _trackRemoved(state, { track: { jitsiTrack } }) {
203
+    if (jitsiTrack) {
204
+        const streamId = jitsiTrack.getStreamId();
205
+
206
+        if (streamId && streamId in state.transforms) {
207
+            const nextTransforms = {
208
+                ...state.transforms
209
+            };
210
+
211
+            delete nextTransforms[streamId];
212
+
213
+            return {
214
+                ...state,
215
+                transforms: nextTransforms
216
+            };
217
+        }
218
+    }
219
+
220
+    return state;
221
+}

+ 9
- 2
react/features/base/participants/components/ParticipantView.native.js Parādīt failu

@@ -117,7 +117,12 @@ type Props = {
117 117
      * stacking space of all {@code Video}s. For more details, refer to the
118 118
      * {@code zOrder} property of the {@code Video} class for React Native.
119 119
      */
120
-    zOrder: number
120
+    zOrder: number,
121
+
122
+    /**
123
+     * Indicates whether zooming (pinch to zoom and/or drag) is enabled.
124
+     */
125
+    zoomEnabled: boolean
121 126
 };
122 127
 
123 128
 /**
@@ -127,6 +132,7 @@ type Props = {
127 132
  * @extends Component
128 133
  */
129 134
 class ParticipantView extends Component<Props> {
135
+
130 136
     /**
131 137
      * Renders the connection status label, if appropriate.
132 138
      *
@@ -235,7 +241,8 @@ class ParticipantView extends Component<Props> {
235 241
                         onPress = { renderVideo ? onPress : undefined }
236 242
                         videoTrack = { videoTrack }
237 243
                         waitForVideoStarted = { waitForVideoStarted }
238
-                        zOrder = { this.props.zOrder } /> }
244
+                        zOrder = { this.props.zOrder }
245
+                        zoomEnabled = { this.props.zoomEnabled } /> }
239 246
 
240 247
                 { renderAvatar
241 248
                     && <Avatar

+ 2
- 1
react/features/large-video/components/LargeVideo.native.js Parādīt failu

@@ -126,7 +126,8 @@ class LargeVideo extends Component<Props, State> {
126 126
                     participantId = { _participantId }
127 127
                     style = { styles.largeVideo }
128 128
                     useConnectivityInfoLabel = { useConnectivityInfoLabel }
129
-                    zOrder = { 0 } />
129
+                    zOrder = { 0 }
130
+                    zoomEnabled = { true } />
130 131
             </DimensionsDetector>
131 132
         );
132 133
     }

Notiek ielāde…
Atcelt
Saglabāt