Add pinch zoom functionality
This commit is contained in:
parent
decbcefbd4
commit
79b7e1641d
|
@ -49,6 +49,18 @@ export const SET_VIDEO_AVAILABLE = Symbol('SET_VIDEO_AVAILABLE');
|
|||
*/
|
||||
export const SET_VIDEO_MUTED = Symbol('SET_VIDEO_MUTED');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to store the last video {@link Transform} applied
|
||||
* to a stream.
|
||||
*
|
||||
* {
|
||||
* type: STORE_VIDEO_TRANSFORM,
|
||||
* streamId: string,
|
||||
* transform: Transform
|
||||
* }
|
||||
*/
|
||||
export const STORE_VIDEO_TRANSFORM = Symbol('STORE_VIDEO_TRANSFORM');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to toggle the local video camera facing mode. In
|
||||
* contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
SET_CAMERA_FACING_MODE,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
|
@ -18,9 +19,9 @@ import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
|||
* @param {boolean} available - True if the local audio is to be marked as
|
||||
* available or false if the local audio is not available.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_AVAILABLE,
|
||||
* available: boolean
|
||||
* }}
|
||||
* type: SET_AUDIO_AVAILABLE,
|
||||
* available: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setAudioAvailable(available: boolean) {
|
||||
return {
|
||||
|
@ -112,6 +113,26 @@ export function setVideoMuted(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to store the last video {@link Transform} applied to a
|
||||
* stream.
|
||||
*
|
||||
* @param {string} streamId - The ID of the stream.
|
||||
* @param {Object} transform - The {@code Transform} to store.
|
||||
* @returns {{
|
||||
* type: STORE_VIDEO_TRANSFORM,
|
||||
* streamId: string,
|
||||
* transform: Object
|
||||
* }}
|
||||
*/
|
||||
export function storeVideoTransform(streamId: string, transform: Object) {
|
||||
return {
|
||||
type: STORE_VIDEO_TRANSFORM,
|
||||
streamId,
|
||||
transform
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the camera facing mode. Most commonly, for example, mobile devices
|
||||
* such as phones have a front/user-facing and a back/environment-facing
|
||||
|
|
|
@ -36,7 +36,12 @@ export default class AbstractVideoTrack extends Component {
|
|||
* of all Videos. For more details, refer to the zOrder property of the
|
||||
* Video class for React Native.
|
||||
*/
|
||||
zOrder: PropTypes.number
|
||||
zOrder: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -80,7 +85,7 @@ export default class AbstractVideoTrack extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const videoTrack = this.state.videoTrack;
|
||||
const { videoTrack } = this.state;
|
||||
let render;
|
||||
|
||||
if (this.props.waitForVideoStarted) {
|
||||
|
@ -108,13 +113,21 @@ export default class AbstractVideoTrack extends Component {
|
|||
const stream
|
||||
= render ? videoTrack.jitsiTrack.getOriginalStream() : null;
|
||||
|
||||
// Actual zoom is currently only enabled if the stream is a desktop
|
||||
// stream.
|
||||
const zoomEnabled
|
||||
= this.props.zoomEnabled
|
||||
&& stream
|
||||
&& videoTrack.videoType === 'desktop';
|
||||
|
||||
return (
|
||||
<Video
|
||||
mirror = { videoTrack && videoTrack.mirror }
|
||||
onPlaying = { this._onVideoPlaying }
|
||||
onPress = { this.props.onPress }
|
||||
stream = { stream }
|
||||
zOrder = { this.props.zOrder } />
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { zoomEnabled } />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -125,7 +138,7 @@ export default class AbstractVideoTrack extends Component {
|
|||
* @returns {void}
|
||||
*/
|
||||
_onVideoPlaying() {
|
||||
const videoTrack = this.props.videoTrack;
|
||||
const { videoTrack } = this.props;
|
||||
|
||||
if (videoTrack && !videoTrack.videoStarted) {
|
||||
this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack));
|
||||
|
|
|
@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { RTCView } from 'react-native-webrtc';
|
||||
|
||||
import { Pressable } from '../../../react';
|
||||
|
||||
import styles from './styles';
|
||||
import VideoTransform from './VideoTransform';
|
||||
|
||||
/**
|
||||
* The React Native {@link Component} which is similar to Web's
|
||||
|
@ -54,7 +53,12 @@ export default class Video extends Component<*> {
|
|||
* values: 0 for the remote video(s) which appear in the background, and
|
||||
* 1 for the local video(s) which appear above the remote video(s).
|
||||
*/
|
||||
zOrder: PropTypes.number
|
||||
zOrder: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -77,29 +81,29 @@ export default class Video extends Component<*> {
|
|||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const { stream } = this.props;
|
||||
const { stream, zoomEnabled } = this.props;
|
||||
|
||||
if (stream) {
|
||||
const streamURL = stream.toURL();
|
||||
|
||||
// XXX The CSS style object-fit that we utilize on Web is not
|
||||
// supported on React Native. Adding objectFit to React Native's
|
||||
// StyleSheet appears to be impossible without hacking and an
|
||||
// unjustified amount of effort. Consequently, I've chosen to define
|
||||
// objectFit on RTCView itself. Anyway, prepare to accommodate a
|
||||
// future definition of objectFit in React Native's StyleSheet.
|
||||
const style = styles.video;
|
||||
const objectFit = (style && style.objectFit) || 'cover';
|
||||
const objectFit
|
||||
= zoomEnabled
|
||||
? 'contain'
|
||||
: (style && style.objectFit) || 'cover';
|
||||
|
||||
return (
|
||||
<Pressable onPress = { this.props.onPress }>
|
||||
<VideoTransform
|
||||
enabled = { zoomEnabled }
|
||||
onPress = { this.props.onPress }
|
||||
streamId = { stream.id }
|
||||
style = { style }>
|
||||
<RTCView
|
||||
mirror = { this.props.mirror }
|
||||
objectFit = { objectFit }
|
||||
streamURL = { streamURL }
|
||||
style = { style }
|
||||
zOrder = { this.props.zOrder } />
|
||||
</Pressable>
|
||||
</VideoTransform>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,714 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { PanResponder, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { storeVideoTransform } from '../../actions';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The default/initial transform (= no transform).
|
||||
*/
|
||||
const DEFAULT_TRANSFORM = {
|
||||
scale: 1,
|
||||
translateX: 0,
|
||||
translateY: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* The minimum scale (magnification) multiplier. 1 is equal to objectFit
|
||||
* = 'contain'.
|
||||
*/
|
||||
const MIN_SCALE = 1;
|
||||
|
||||
/*
|
||||
* The max distance from the edge of the screen where we let the user move the
|
||||
* view to. This is large enough now to let the user drag the view to a position
|
||||
* where no other displayed components cover it (such as filmstrip). If a
|
||||
* ViewPort (hint) support is added to the LargeVideo component then this
|
||||
* contant will not be necessary anymore.
|
||||
*/
|
||||
const MAX_OFFSET = 100;
|
||||
|
||||
/**
|
||||
* The max allowed scale (magnification) multiplier.
|
||||
*/
|
||||
const MAX_SCALE = 5;
|
||||
|
||||
/**
|
||||
* The length of a minimum movement after which we consider a gesture a move
|
||||
* instead of a tap/long tap.
|
||||
*/
|
||||
const MOVE_THRESHOLD_DISMISSES_TOUCH = 2;
|
||||
|
||||
/**
|
||||
* A tap timeout after which we consider a gesture a long tap and will not
|
||||
* trigger onPress (unless long tap gesture support is added in the future).
|
||||
*/
|
||||
const TAP_TIMEOUT_MS = 400;
|
||||
|
||||
/**
|
||||
* Type of a transform object this component is capable of handling.
|
||||
*/
|
||||
type Transform = {
|
||||
scale: number,
|
||||
translateX: number,
|
||||
translateY: number
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The children components of this view.
|
||||
*/
|
||||
children: Object,
|
||||
|
||||
/**
|
||||
* Transformation is only enabled when this flag is true.
|
||||
*/
|
||||
enabled: boolean,
|
||||
|
||||
/**
|
||||
* Function to invoke when a press event is detected.
|
||||
*/
|
||||
onPress?: Function,
|
||||
|
||||
/**
|
||||
* The id of the current stream that is displayed.
|
||||
*/
|
||||
streamId: string,
|
||||
|
||||
/**
|
||||
* Style of the top level transformable view.
|
||||
*/
|
||||
style: Object,
|
||||
|
||||
/**
|
||||
* The stored transforms retreived from Redux to be initially applied
|
||||
* to different streams.
|
||||
*/
|
||||
_transforms: Object,
|
||||
|
||||
/**
|
||||
* Action to dispatch when the component is unmounted.
|
||||
*/
|
||||
_onUnmount: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current (non-transformed) layout of the View.
|
||||
*/
|
||||
layout: ?Object,
|
||||
|
||||
/**
|
||||
* The current transform that is applied.
|
||||
*/
|
||||
transform: Transform
|
||||
};
|
||||
|
||||
/**
|
||||
* An container that captures gestures such as pinch&zoom, touch or move.
|
||||
*/
|
||||
class VideoTransform extends Component<Props, State> {
|
||||
/**
|
||||
* The gesture handler object.
|
||||
*/
|
||||
gestureHandlers: PanResponder;
|
||||
|
||||
/**
|
||||
* The initial distance of the fingers on pinch start.
|
||||
*/
|
||||
initialDistance: number;
|
||||
|
||||
/**
|
||||
* The initial position of the finger on touch start.
|
||||
*/
|
||||
initialPosition: {
|
||||
x: number,
|
||||
y: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Time of the last tap.
|
||||
*/
|
||||
lastTap: number;
|
||||
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
layout: null,
|
||||
transform: DEFAULT_TRANSFORM
|
||||
};
|
||||
|
||||
this._getTransformStyle = this._getTransformStyle.bind(this);
|
||||
this._onGesture = this._onGesture.bind(this);
|
||||
this._onLayout = this._onLayout.bind(this);
|
||||
this._onMoveShouldSetPanResponder
|
||||
= this._onMoveShouldSetPanResponder.bind(this);
|
||||
this._onPanResponderGrant = this._onPanResponderGrant.bind(this);
|
||||
this._onPanResponderMove = this._onPanResponderMove.bind(this);
|
||||
this._onPanResponderRelease = this._onPanResponderRelease.bind(this);
|
||||
this._onStartShouldSetPanResponder
|
||||
= this._onStartShouldSetPanResponder.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.gestureHandlers = PanResponder.create({
|
||||
onPanResponderGrant: this._onPanResponderGrant,
|
||||
onPanResponderMove: this._onPanResponderMove,
|
||||
onPanResponderRelease: this._onPanResponderRelease,
|
||||
onPanResponderTerminationRequest: () => true,
|
||||
onMoveShouldSetPanResponder: this._onMoveShouldSetPanResponder,
|
||||
onShouldBlockNativeResponder: () => false,
|
||||
onStartShouldSetPanResponder: this._onStartShouldSetPanResponder
|
||||
});
|
||||
|
||||
const { streamId } = this.props;
|
||||
|
||||
this._restoreTransform(streamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillReceiveProps.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillReceiveProps({ streamId: newStreamId }) {
|
||||
if (this.props.streamId !== newStreamId) {
|
||||
this._storeTransform();
|
||||
this._restoreTransform(newStreamId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._storeTransform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the empty component that captures the gestures.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { children, style } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout = { this._onLayout }
|
||||
pointerEvents = 'box-only'
|
||||
style = { [
|
||||
styles.videoTransformedViewContaier,
|
||||
style
|
||||
] }
|
||||
{ ...this.gestureHandlers.panHandlers }>
|
||||
<View
|
||||
style = { [
|
||||
styles.videoTranformedView,
|
||||
this._getTransformStyle()
|
||||
] }>
|
||||
{ children }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the new transformation to be applied by merging the current
|
||||
* transform values with the newly received incremental values.
|
||||
*
|
||||
* @param {Transform} transform - The new transform object.
|
||||
* @private
|
||||
* @returns {Transform}
|
||||
*/
|
||||
_calculateTransformIncrement(transform: Transform) {
|
||||
let {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
} = this.state.transform;
|
||||
const {
|
||||
scale: newScale,
|
||||
translateX: newTranslateX,
|
||||
translateY: newTranslateY
|
||||
} = transform;
|
||||
|
||||
// Note: We don't limit MIN_SCALE here yet, as we need to detect a scale
|
||||
// down gesture even if the scale is already at MIN_SCALE to let the
|
||||
// user return the screen to center with that gesture. Scale is limited
|
||||
// to MIN_SCALE right before it gets applied.
|
||||
scale = Math.min(scale * (newScale || 1), MAX_SCALE);
|
||||
|
||||
translateX = translateX + ((newTranslateX || 0) / scale);
|
||||
translateY = translateY + ((newTranslateY || 0) / scale);
|
||||
|
||||
return {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
};
|
||||
}
|
||||
|
||||
_didMove: Object => boolean
|
||||
|
||||
/**
|
||||
* Determines if there was large enough movement to be handled.
|
||||
*
|
||||
* @param {Object} gestureState - The gesture state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_didMove({ dx, dy }) {
|
||||
return Math.abs(dx) > MOVE_THRESHOLD_DISMISSES_TOUCH
|
||||
|| Math.abs(dy) > MOVE_THRESHOLD_DISMISSES_TOUCH;
|
||||
}
|
||||
|
||||
_getTouchDistance: Object => number;
|
||||
|
||||
/**
|
||||
* Calculates the touch distance on a pinch event.
|
||||
*
|
||||
* @param {Object} evt - The touch event.
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_getTouchDistance({ nativeEvent: { touches } }) {
|
||||
const dx = Math.abs(touches[0].pageX - touches[1].pageX);
|
||||
const dy = Math.abs(touches[0].pageY - touches[1].pageY);
|
||||
|
||||
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
|
||||
}
|
||||
|
||||
_getTouchPosition: Object => Object
|
||||
|
||||
/**
|
||||
* Calculates the position of the touch event.
|
||||
*
|
||||
* @param {Object} evt - The touch event.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTouchPosition({ nativeEvent: { touches } }) {
|
||||
return {
|
||||
x: touches[0].pageX,
|
||||
y: touches[0].pageY
|
||||
};
|
||||
}
|
||||
|
||||
_getTransformStyle: () => Object
|
||||
|
||||
/**
|
||||
* Generates a transform style object to be used on the component.
|
||||
*
|
||||
* @returns {{string: Array<{string: number}>}}
|
||||
*/
|
||||
_getTransformStyle() {
|
||||
const { enabled } = this.props;
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
} = this.state.transform;
|
||||
|
||||
return {
|
||||
transform: [
|
||||
{ scale },
|
||||
{ translateX },
|
||||
{ translateY }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the move matrix and then applies the transformation to the
|
||||
* component (updates state).
|
||||
*
|
||||
* Note: Points A (top-left) and D (bottom-right) are opposite points of
|
||||
* the View rectangle.
|
||||
*
|
||||
* @param {Transform} transform - The transformation object.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_limitAndApplyTransformation(transform: Transform) {
|
||||
const { layout } = this.state;
|
||||
|
||||
if (layout) {
|
||||
const { scale } = this.state.transform;
|
||||
const { scale: newScaleUnlimited } = transform;
|
||||
let {
|
||||
translateX: newTranslateX,
|
||||
translateY: newTranslateY
|
||||
} = transform;
|
||||
|
||||
// Scale is only limited to MIN_SCALE here to detect downscale
|
||||
// gesture later.
|
||||
const newScale = Math.max(newScaleUnlimited, MIN_SCALE);
|
||||
|
||||
// The A and D points of the original View (before transform).
|
||||
const originalLayout = {
|
||||
a: {
|
||||
x: layout.x,
|
||||
y: layout.y
|
||||
},
|
||||
d: {
|
||||
x: layout.x + layout.width,
|
||||
y: layout.y + layout.height
|
||||
}
|
||||
};
|
||||
|
||||
// The center point (midpoint) of the transformed View.
|
||||
const transformedCenterPoint = {
|
||||
x: ((layout.x + layout.width) / 2) + (newTranslateX * newScale),
|
||||
y: ((layout.y + layout.height) / 2) + (newTranslateY * newScale)
|
||||
};
|
||||
|
||||
// The size of the transformed View.
|
||||
const transformedSize = {
|
||||
height: layout.height * newScale,
|
||||
width: layout.width * newScale
|
||||
};
|
||||
|
||||
// The A and D points of the transformed View.
|
||||
const transformedLayout = {
|
||||
a: {
|
||||
x: transformedCenterPoint.x - (transformedSize.width / 2),
|
||||
y: transformedCenterPoint.y - (transformedSize.height / 2)
|
||||
},
|
||||
d: {
|
||||
x: transformedCenterPoint.x + (transformedSize.width / 2),
|
||||
y: transformedCenterPoint.y + (transformedSize.height / 2)
|
||||
}
|
||||
};
|
||||
|
||||
let _MAX_OFFSET = MAX_OFFSET;
|
||||
|
||||
if (newScaleUnlimited < scale) {
|
||||
// This is a negative scale event so we dynamycally reduce the
|
||||
// MAX_OFFSET to get the screen back to the center on
|
||||
// downscaling.
|
||||
_MAX_OFFSET = Math.min(MAX_OFFSET, MAX_OFFSET * (newScale - 1));
|
||||
}
|
||||
|
||||
// Correct move matrix if it goes out of the view
|
||||
// too much (_MAX_OFFSET).
|
||||
newTranslateX
|
||||
-= Math.max(
|
||||
transformedLayout.a.x - originalLayout.a.x - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateX
|
||||
+= Math.max(
|
||||
originalLayout.d.x - transformedLayout.d.x - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateY
|
||||
-= Math.max(
|
||||
transformedLayout.a.y - originalLayout.a.y - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateY
|
||||
+= Math.max(
|
||||
originalLayout.d.y - transformedLayout.d.y - _MAX_OFFSET,
|
||||
0);
|
||||
|
||||
this.setState({
|
||||
transform: {
|
||||
scale: newScale,
|
||||
translateX: Math.round(newTranslateX),
|
||||
translateY: Math.round(newTranslateY)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onGesture: (string, ?Object | number) => void
|
||||
|
||||
/**
|
||||
* Handles gestures and converts them to transforms.
|
||||
*
|
||||
* Currently supported gestures:
|
||||
* - scale (punch&zoom-type scale).
|
||||
* - move
|
||||
* - press.
|
||||
*
|
||||
* Note: This component supports onPress solely to overcome the problem of
|
||||
* not being able to register gestures via the PanResponder due to the fact
|
||||
* that the entire Conference component was a single touch responder
|
||||
* component in the past (see base/react/.../Container with an onPress
|
||||
* event) - and stock touch responder components seem to have exclusive
|
||||
* priority in handling touches in React.
|
||||
*
|
||||
* @param {string} type - The type of the gesture.
|
||||
* @param {?Object | number} value - The value of the gesture, if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGesture(type, value) {
|
||||
let transform;
|
||||
|
||||
switch (type) {
|
||||
case 'move':
|
||||
transform = {
|
||||
...DEFAULT_TRANSFORM,
|
||||
translateX: value.x,
|
||||
translateY: value.y
|
||||
};
|
||||
break;
|
||||
case 'scale':
|
||||
transform = {
|
||||
...DEFAULT_TRANSFORM,
|
||||
scale: value
|
||||
};
|
||||
break;
|
||||
|
||||
case 'press': {
|
||||
const { onPress } = this.props;
|
||||
|
||||
typeof onPress === 'function' && onPress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (transform) {
|
||||
this._limitAndApplyTransformation(
|
||||
this._calculateTransformIncrement(transform));
|
||||
}
|
||||
|
||||
this.lastTap = 0;
|
||||
}
|
||||
|
||||
_onLayout: Object => void
|
||||
|
||||
/**
|
||||
* Callback for the onLayout of the component.
|
||||
*
|
||||
* @param {Object} event - The native props of the onLayout event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLayout({ nativeEvent: { layout: { x, y, width, height } } }) {
|
||||
this.setState({
|
||||
layout: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onMoveShouldSetPanResponder: (Object, Object) => boolean
|
||||
|
||||
/**
|
||||
* Function to decide whether the responder should respond to a move event.
|
||||
*
|
||||
* @param {Object} evt - The event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onMoveShouldSetPanResponder(evt, gestureState) {
|
||||
return this.props.enabled
|
||||
&& (this._didMove(gestureState)
|
||||
|| gestureState.numberActiveTouches === 2);
|
||||
}
|
||||
|
||||
_onPanResponderGrant: (Object, Object) => void
|
||||
|
||||
/**
|
||||
* Calculates the initial touch distance.
|
||||
*
|
||||
* @param {Object} evt - Touch event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderGrant(evt, { numberActiveTouches }) {
|
||||
if (numberActiveTouches === 1) {
|
||||
this.initialPosition = this._getTouchPosition(evt);
|
||||
this.lastTap = Date.now();
|
||||
|
||||
} else if (numberActiveTouches === 2) {
|
||||
this.initialDistance = this._getTouchDistance(evt);
|
||||
}
|
||||
}
|
||||
|
||||
_onPanResponderMove: (Object, Object) => void
|
||||
|
||||
/**
|
||||
* Handles the PanResponder move (touch move) event.
|
||||
*
|
||||
* @param {Object} evt - Touch event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderMove(evt, gestureState) {
|
||||
if (gestureState.numberActiveTouches === 2) {
|
||||
// this is a zoom event
|
||||
if (
|
||||
this.initialDistance === undefined
|
||||
|| isNaN(this.initialDistance)
|
||||
) {
|
||||
// there is no initial distance because the user started
|
||||
// with only one finger. We calculate it now.
|
||||
this.initialDistance = this._getTouchDistance(evt);
|
||||
} else {
|
||||
const distance = this._getTouchDistance(evt);
|
||||
const scale = distance / (this.initialDistance || 1);
|
||||
|
||||
this.initialDistance = distance;
|
||||
|
||||
this._onGesture('scale', scale);
|
||||
}
|
||||
} else if (gestureState.numberActiveTouches === 1
|
||||
&& isNaN(this.initialDistance)
|
||||
&& this._didMove(gestureState)) {
|
||||
// this is a move event
|
||||
const position = this._getTouchPosition(evt);
|
||||
const move = {
|
||||
x: position.x - this.initialPosition.x,
|
||||
y: position.y - this.initialPosition.y
|
||||
};
|
||||
|
||||
this.initialPosition = position;
|
||||
|
||||
this._onGesture('move', move);
|
||||
}
|
||||
}
|
||||
|
||||
_onPanResponderRelease: () => void
|
||||
|
||||
/**
|
||||
* Handles the PanResponder gesture end event.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderRelease() {
|
||||
if (this.lastTap && Date.now() - this.lastTap < TAP_TIMEOUT_MS) {
|
||||
this._onGesture('press');
|
||||
}
|
||||
delete this.initialDistance;
|
||||
delete this.initialPosition;
|
||||
}
|
||||
|
||||
_onStartShouldSetPanResponder: () => boolean
|
||||
|
||||
/**
|
||||
* Function to decide whether the responder should respond to a start
|
||||
* (thouch) event.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onStartShouldSetPanResponder() {
|
||||
return typeof this.props.onPress === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the last applied transform when the component is mounted, or
|
||||
* a new stream is about to be rendered.
|
||||
*
|
||||
* @param {string} streamId - The stream id to restore transform for.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_restoreTransform(streamId) {
|
||||
const { enabled, _transforms } = this.props;
|
||||
|
||||
if (enabled) {
|
||||
const initialTransform = _transforms[streamId];
|
||||
|
||||
if (initialTransform) {
|
||||
this.setState({
|
||||
transform: initialTransform
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores/saves the current transform when the component is destroyed, or a
|
||||
* new stream is about to be rendered.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_storeTransform() {
|
||||
const { _onUnmount, enabled, streamId } = this.props;
|
||||
|
||||
if (enabled) {
|
||||
_onUnmount(streamId, this.state.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some action to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _onUnmount: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches actions to store the last applied transform to a video.
|
||||
*
|
||||
* @param {string} streamId - The ID of the stream.
|
||||
* @param {Transform} transform - The last applied transform.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUnmount(streamId, transform) {
|
||||
dispatch(storeVideoTransform(streamId, transform));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the component's props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _transforms: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
/**
|
||||
* The stored transforms retreived from Redux to be initially applied to
|
||||
* different streams.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
_transforms: state['features/base/media'].video.transforms
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(VideoTransform);
|
|
@ -6,6 +6,23 @@ import { ColorPalette } from '../../../styles';
|
|||
* The styles of the feature base/media.
|
||||
*/
|
||||
export default StyleSheet.create({
|
||||
|
||||
/**
|
||||
* Base style of the transformed video view.
|
||||
*/
|
||||
videoTranformedView: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* A basic style to avoid rendering a transformed view off the component,
|
||||
* that can be visible on special occasions, such as during device rotate
|
||||
* animation, or PiP mode.
|
||||
*/
|
||||
videoTransformedViewContaier: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
/**
|
||||
* Make {@code Video} fill its container.
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference';
|
||||
import { ReducerRegistry } from '../redux';
|
||||
import { TRACK_REMOVED } from '../tracks';
|
||||
|
||||
import {
|
||||
SET_AUDIO_AVAILABLE,
|
||||
|
@ -8,6 +10,7 @@ import {
|
|||
SET_CAMERA_FACING_MODE,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE } from './constants';
|
||||
|
@ -73,7 +76,13 @@ function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
|
|||
const VIDEO_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
facingMode: CAMERA_FACING_MODE.USER,
|
||||
muted: 0
|
||||
muted: 0,
|
||||
|
||||
/**
|
||||
* The video {@link Transform}s applied to {@code MediaStream}s by
|
||||
* {@code id} i.e. "pinch to zoom".
|
||||
*/
|
||||
transforms: {}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -87,6 +96,10 @@ const VIDEO_INITIAL_MEDIA_STATE = {
|
|||
*/
|
||||
function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED:
|
||||
case CONFERENCE_LEFT:
|
||||
return _clearAllVideoTransforms(state);
|
||||
|
||||
case SET_CAMERA_FACING_MODE:
|
||||
return {
|
||||
...state,
|
||||
|
@ -105,6 +118,9 @@ function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
|
|||
muted: action.muted
|
||||
};
|
||||
|
||||
case STORE_VIDEO_TRANSFORM:
|
||||
return _storeVideoTransform(state, action);
|
||||
|
||||
case TOGGLE_CAMERA_FACING_MODE: {
|
||||
let cameraFacingMode = state.facingMode;
|
||||
|
||||
|
@ -119,6 +135,9 @@ function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
|
|||
};
|
||||
}
|
||||
|
||||
case TRACK_REMOVED:
|
||||
return _trackRemoved(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -138,3 +157,65 @@ ReducerRegistry.register('features/base/media', combineReducers({
|
|||
audio: _audio,
|
||||
video: _video
|
||||
}));
|
||||
|
||||
/**
|
||||
* Removes all stored video {@link Transform}s.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _clearAllVideoTransforms(state) {
|
||||
return {
|
||||
...state,
|
||||
transforms: VIDEO_INITIAL_MEDIA_STATE.transforms
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the last applied transform to a stream.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @param {Object} action - The redux action {@link STORE_VIDEO_TRANSFORM}.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _storeVideoTransform(state, { streamId, transform }) {
|
||||
return {
|
||||
...state,
|
||||
transforms: {
|
||||
...state.transforms,
|
||||
[streamId]: transform
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the stored video {@link Transform} associated with a
|
||||
* {@code MediaStream} when its respective track is removed.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @param {Object} action - The redux action {@link TRACK_REMOVED}.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _trackRemoved(state, { track: { jitsiTrack } }) {
|
||||
if (jitsiTrack) {
|
||||
const streamId = jitsiTrack.getStreamId();
|
||||
|
||||
if (streamId && streamId in state.transforms) {
|
||||
const nextTransforms = {
|
||||
...state.transforms
|
||||
};
|
||||
|
||||
delete nextTransforms[streamId];
|
||||
|
||||
return {
|
||||
...state,
|
||||
transforms: nextTransforms
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -117,7 +117,12 @@ type Props = {
|
|||
* stacking space of all {@code Video}s. For more details, refer to the
|
||||
* {@code zOrder} property of the {@code Video} class for React Native.
|
||||
*/
|
||||
zOrder: number
|
||||
zOrder: number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -127,6 +132,7 @@ type Props = {
|
|||
* @extends Component
|
||||
*/
|
||||
class ParticipantView extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Renders the connection status label, if appropriate.
|
||||
*
|
||||
|
@ -235,7 +241,8 @@ class ParticipantView extends Component<Props> {
|
|||
onPress = { renderVideo ? onPress : undefined }
|
||||
videoTrack = { videoTrack }
|
||||
waitForVideoStarted = { waitForVideoStarted }
|
||||
zOrder = { this.props.zOrder } /> }
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
|
||||
{ renderAvatar
|
||||
&& <Avatar
|
||||
|
|
|
@ -126,7 +126,8 @@ class LargeVideo extends Component<Props, State> {
|
|||
participantId = { _participantId }
|
||||
style = { styles.largeVideo }
|
||||
useConnectivityInfoLabel = { useConnectivityInfoLabel }
|
||||
zOrder = { 0 } />
|
||||
zOrder = { 0 }
|
||||
zoomEnabled = { true } />
|
||||
</DimensionsDetector>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue