diff --git a/@types/index.d.ts b/@types/index.d.ts index c3266644..41f2a38d 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -271,6 +271,11 @@ declare module "opentok-react-native" { */ resolution?: "1280x720" | "640x480" | "352x288"; + /** + * Publisher view scale behavior. Defaults to "fill". + */ + scaleBehavior?: "fill" | "fit"; + /** * If this property is set to false, the video subsystem will not be initialized for the publisher, and setting the publishVideo property will have no effect. If your application does not require the use of video, it is recommended to set this property rather than use the publishVideo property, which only temporarily disables the video track. */ @@ -347,6 +352,11 @@ declare module "opentok-react-native" { } interface OTSubscriberProperties { + /** + * Subscriber view scale behavior. Defaults to "fill". + */ + scaleBehavior?: "fill" | "fit"; + /** * Whether to subscribe to audio. */ diff --git a/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java b/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java index 438a1fb2..417b28ed 100644 --- a/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java +++ b/android/src/main/java/com/opentokreactnative/OTPublisherLayout.java @@ -5,7 +5,6 @@ import android.opengl.GLSurfaceView; import com.facebook.react.uimanager.ThemedReactContext; -import com.opentok.android.BaseVideoRenderer; import com.opentok.android.Publisher; import java.util.concurrent.ConcurrentHashMap; @@ -39,8 +38,6 @@ public void createPublisherView(String publisherId) { if (androidZOrderMap.get(mPublisher.getSession().getSessionId()) != null) { zOrder = androidZOrderMap.get(mPublisher.getSession().getSessionId()); } - mPublisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, - BaseVideoRenderer.STYLE_VIDEO_FILL); FrameLayout mPublisherViewContainer = new FrameLayout(getContext()); if (pubOrSub.equals("publisher") && mPublisher.getView() instanceof GLSurfaceView) { if (zOrder.equals("mediaOverlay")) { diff --git a/android/src/main/java/com/opentokreactnative/OTSessionManager.java b/android/src/main/java/com/opentokreactnative/OTSessionManager.java index 07ab96ab..fc9ee38a 100644 --- a/android/src/main/java/com/opentokreactnative/OTSessionManager.java +++ b/android/src/main/java/com/opentokreactnative/OTSessionManager.java @@ -22,6 +22,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableArray; +import com.opentok.android.BaseVideoRenderer; import com.opentok.android.Session; import com.opentok.android.Connection; import com.opentok.android.Publisher; @@ -108,7 +109,7 @@ public boolean isCamera2Capable() { } }) .connectionEventsSuppressed(connectionEventsSuppressed) - // Note: setCustomIceServers is an additional property not supported at the moment. + // Note: setCustomIceServers is an additional property not supported at the moment. // .setCustomIceServers(serverList, config) .setIpWhitelist(ipWhitelist) .setProxyUrl(proxyUrl) @@ -151,6 +152,7 @@ public void initPublisher(String publisherId, ReadableMap properties, Callback c String resolution = properties.getString("resolution"); Boolean publishAudio = properties.getBoolean("publishAudio"); Boolean publishVideo = properties.getBoolean("publishVideo"); + String scaleBehavior = properties.getString("scaleBehavior"); String videoSource = properties.getString("videoSource"); Publisher mPublisher = null; if (videoSource.equals("screen")) { @@ -184,6 +186,8 @@ public void initPublisher(String publisherId, ReadableMap properties, Callback c mPublisher.setAudioFallbackEnabled(audioFallbackEnabled); mPublisher.setPublishVideo(publishVideo); mPublisher.setPublishAudio(publishAudio); + mPublisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, scaleBehavior.equals("fill") ? + BaseVideoRenderer.STYLE_VIDEO_FILL : BaseVideoRenderer.STYLE_VIDEO_FIT); ConcurrentHashMap mPublishers = sharedState.getPublishers(); mPublishers.put(publisherId, mPublisher); callback.invoke(); @@ -226,6 +230,8 @@ public void subscribeToStream(String streamId, String sessionId, ReadableMap pro mSubscriber.setStreamListener(this); mSubscriber.setSubscribeToAudio(properties.getBoolean("subscribeToAudio")); mSubscriber.setSubscribeToVideo(properties.getBoolean("subscribeToVideo")); + mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, properties.getString("scaleBehavior").equals("fill") ? + BaseVideoRenderer.STYLE_VIDEO_FILL : BaseVideoRenderer.STYLE_VIDEO_FIT); if (properties.hasKey("preferredFrameRate")) { mSubscriber.setPreferredFrameRate((float) properties.getDouble("preferredFrameRate")); } @@ -305,6 +311,28 @@ public void publishVideo(String publisherId, Boolean publishVideo) { } } + @ReactMethod + public void setPublisherScaleBehavior(String publisherId, String scaleBehavior) { + + ConcurrentHashMap mPublishers = sharedState.getPublishers(); + Publisher mPublisher = mPublishers.get(publisherId); + if (mPublisher != null) { + mPublisher.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, scaleBehavior.equals("fill") ? + BaseVideoRenderer.STYLE_VIDEO_FILL : BaseVideoRenderer.STYLE_VIDEO_FIT); + } + } + + @ReactMethod + public void setSubscriberScaleBehavior(String streamId, String scaleBehavior) { + + ConcurrentHashMap mSubscribers = sharedState.getSubscribers(); + Subscriber mSubscriber = mSubscribers.get(streamId); + if (mSubscriber != null) { + mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, scaleBehavior.equals("fill") ? + BaseVideoRenderer.STYLE_VIDEO_FILL : BaseVideoRenderer.STYLE_VIDEO_FIT); + } + } + @ReactMethod public void subscribeToAudio(String streamId, Boolean subscribeToAudio) { diff --git a/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java b/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java index ca30a458..0ce46e88 100644 --- a/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java +++ b/android/src/main/java/com/opentokreactnative/OTSubscriberLayout.java @@ -6,7 +6,6 @@ import android.widget.FrameLayout; import com.facebook.react.uimanager.ThemedReactContext; -import com.opentok.android.BaseVideoRenderer; import com.opentok.android.Subscriber; import java.util.concurrent.ConcurrentHashMap; @@ -45,8 +44,6 @@ public void createSubscriberView(String streamId) { if (mSubscriber.getView().getParent() != null) { ((ViewGroup)mSubscriber.getView().getParent()).removeView(mSubscriber.getView()); } - mSubscriber.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, - BaseVideoRenderer.STYLE_VIDEO_FILL); if (pubOrSub.equals("subscriber") && mSubscriber.getView() instanceof GLSurfaceView) { if (zOrder.equals("mediaOverlay")) { ((GLSurfaceView) mSubscriber.getView()).setZOrderMediaOverlay(true); diff --git a/docs/OTPublisher.md b/docs/OTPublisher.md index f11ae2ed..92739054 100644 --- a/docs/OTPublisher.md +++ b/docs/OTPublisher.md @@ -32,6 +32,8 @@ * **publishVideo** (Boolean) — Whether to publish video. + * **scaleBehavior** (String) - The scale behavior of the publisher view. By default, the value is "fill" which will scale the video to fill the entire view, with cropping as needed. The value "fit" will shrink the video (pillarboxing), so that the entire video is contained in the view. + * **resolution** (String) - The desired resolution of the video. The format of the string is "widthxheight", where the width and height are represented in pixels. Valid values are "1280x720", "640x480", and "352x288". The published video will only use the desired resolution if the client configuration supports it. Some devices and clients do not support each of these resolution settings. * **videoTrack** (Boolean) — If this property is set to false, the video subsystem will not be initialized for the publisher, and setting the publishVideo property will have no effect. If your application does not require the use of video, it is recommended to set this property rather than use the publishVideo property, which only temporarily disables the video track. @@ -99,4 +101,4 @@ Please keep in mind that `OT` is not the same as `OT` in the JS SDK, the `OT` in * **streamCreated** (Object) — Sent when the publisher starts streaming. - * **streamDestroyed** (Object) - Sent when the publisher stops streaming. \ No newline at end of file + * **streamDestroyed** (Object) - Sent when the publisher stops streaming. diff --git a/docs/OTSubscriber.md b/docs/OTSubscriber.md index c0cde8ef..b5a1811b 100644 --- a/docs/OTSubscriber.md +++ b/docs/OTSubscriber.md @@ -11,6 +11,8 @@ | children | Function | No | A render prop allowing individual rendering of each stream ## Properties + * **scaleBehavior** (String) - The scale behavior of the subscriber view. By default, the value is "fill" which will scale the video to fill the entire view, with cropping as needed. The value "fit" will shrink the video (pillarboxing), so that the entire video is contained in the view. + * **subscribeToAudio** (Boolean) — Whether to subscribe to audio. * **subscribeToVideo** (Boolean) — Whether to subscribe video. diff --git a/ios/OpenTokReactNative/OTSessionManager.m b/ios/OpenTokReactNative/OTSessionManager.m index 42208d17..a81e362f 100644 --- a/ios/OpenTokReactNative/OTSessionManager.m +++ b/ios/OpenTokReactNative/OTSessionManager.m @@ -47,6 +47,12 @@ @interface RCT_EXTERN_MODULE(OTSessionManager, RCTEventEmitter) RCT_EXTERN_METHOD(publishVideo: (NSString*)publisherId pubVideo:(BOOL)pubVideo) +RCT_EXTERN_METHOD(setPublisherScaleBehavior: + (NSString*)publisherId + scaleBehavior:(NSString*)scaleBehavior) +RCT_EXTERN_METHOD(setSubscriberScaleBehavior: + (NSString*)streamId + scaleBehavior:(NSString*)scaleBehavior) RCT_EXTERN_METHOD(subscribeToAudio: (NSString*)streamId subAudio:(BOOL)subAudio) diff --git a/ios/OpenTokReactNative/OTSessionManager.swift b/ios/OpenTokReactNative/OTSessionManager.swift index df7c28e9..2d821eb5 100644 --- a/ios/OpenTokReactNative/OTSessionManager.swift +++ b/ios/OpenTokReactNative/OTSessionManager.swift @@ -101,6 +101,7 @@ class OTSessionManager: RCTEventEmitter { publisher.publishAudio = Utils.sanitizeBooleanProperty(properties["publishAudio"] as Any); publisher.publishVideo = Utils.sanitizeBooleanProperty(properties["publishVideo"] as Any); publisher.audioLevelDelegate = self; + publisher.viewScaleBehavior = Utils.sanitizeScaleBehavior(properties["scaleBehavior"] as Any); callback([NSNull()]); } } @@ -151,6 +152,7 @@ class OTSessionManager: RCTEventEmitter { subscriber.subscribeToVideo = Utils.sanitizeBooleanProperty(properties["subscribeToVideo"] as Any); subscriber.preferredFrameRate = Utils.sanitizePreferredFrameRate(properties["preferredFrameRate"] as Any); subscriber.preferredResolution = Utils.sanitizePreferredResolution(properties["preferredResolution"] as Any); + subscriber.viewScaleBehavior = Utils.sanitizeScaleBehavior(properties["scaleBehavior"] as Any); if let err = error { self.dispatchErrorViaCallback(callback, error: err) } else { @@ -200,6 +202,16 @@ class OTSessionManager: RCTEventEmitter { publisher.publishVideo = pubVideo; } + @objc func setPublisherScaleBehavior(_ publisherId: String, scaleBehavior: NSString) -> Void { + guard let publisher = OTRN.sharedState.publishers[publisherId] else { return } + publisher.viewScaleBehavior = Utils.sanitizeScaleBehavior(scaleBehavior); + } + + @objc func setSubscriberScaleBehavior(_ streamId: String, scaleBehavior: NSString) -> Void { + guard let subscriber = OTRN.sharedState.subscribers[streamId] else { return } + subscriber.viewScaleBehavior = Utils.sanitizeScaleBehavior(scaleBehavior); + } + @objc func subscribeToAudio(_ streamId: String, subAudio: Bool) -> Void { guard let subscriber = OTRN.sharedState.subscribers[streamId] else { return } subscriber.subscribeToAudio = subAudio; diff --git a/ios/OpenTokReactNative/Utils/Utils.swift b/ios/OpenTokReactNative/Utils/Utils.swift index ceb50c9f..ebe4a85c 100644 --- a/ios/OpenTokReactNative/Utils/Utils.swift +++ b/ios/OpenTokReactNative/Utils/Utils.swift @@ -36,6 +36,11 @@ class Utils { return CGSize(width: preferredRes["width"] as! CGFloat, height: preferredRes["height"] as! CGFloat); } + static func sanitizeScaleBehavior(_ scaleBehavior: Any) -> OTVideoViewScaleBehavior { + guard let sanitizedScaleBehavior = scaleBehavior as? String else { return .fill; } + return sanitizedScaleBehavior == "fill" ? .fill : .fit; + } + static func sanitizeBooleanProperty(_ property: Any) -> Bool { guard let prop = property as? Bool else { return true; } return prop; diff --git a/src/OTPublisher.js b/src/OTPublisher.js index 919f804e..67661930 100644 --- a/src/OTPublisher.js +++ b/src/OTPublisher.js @@ -25,8 +25,8 @@ class OTPublisher extends Component { this.componentEvents = { sessionConnected: Platform.OS === 'android' ? 'session:onConnected' : 'session:sessionDidConnect', }; - this.componentEventsArray = Object.values(this.componentEvents); - this.otrnEventHandler = getOtrnErrorEventHandler(this.props.eventHandlers); + this.componentEventsArray = Object.values(this.componentEvents); + this.otrnEventHandler = getOtrnErrorEventHandler(this.props.eventHandlers); this.publisherEvents = sanitizePublisherEvents(this.state.publisherId, this.props.eventHandlers); setNativeEvents(this.publisherEvents); OT.setJSComponentEvents(this.componentEventsArray); @@ -50,12 +50,15 @@ class OTPublisher extends Component { const value = useDefault(this.props.properties[key], defaultValue); if (key === 'cameraPosition') { OT.changeCameraPosition(this.state.publisherId, value); + } else if (key === 'scaleBehavior') { + OT.setPublisherScaleBehavior(this.state.publisherId, value); } else { - OT[key](this.state.publisherId, value); + OT[key](this.state.publisherId, value); } } }; + updatePublisherProperty('scaleBehavior', 'fill'); updatePublisherProperty('publishAudio', true); updatePublisherProperty('publishVideo', true); updatePublisherProperty('cameraPosition', 'front'); diff --git a/src/OTSubscriber.js b/src/OTSubscriber.js index 943de5ad..31098a24 100644 --- a/src/OTSubscriber.js +++ b/src/OTSubscriber.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { isNull, isUndefined, each, isEqual, isEmpty } from 'underscore'; import { OT, nativeEvents, setNativeEvents, removeNativeEvents } from './OT'; import OTSubscriberView from './views/OTSubscriberView'; -import { sanitizeSubscriberEvents, sanitizeProperties, sanitizeFrameRate, sanitizeResolution } from './helpers/OTSubscriberHelper'; +import { sanitizeSubscriberEvents, sanitizeProperties, sanitizeFrameRate, sanitizeResolution, sanitizeScaleBehavior } from './helpers/OTSubscriberHelper'; import { getOtrnErrorEventHandler, sanitizeBooleanProperty } from './helpers/OTHelper'; import OTContext from './contexts/OTContext'; @@ -40,7 +40,10 @@ export default class OTSubscriber extends Component { const { streamProperties } = this.props; if (!isEqual(this.state.streamProperties, streamProperties)) { each(streamProperties, (individualStreamProperties, streamId) => { - const { subscribeToAudio, subscribeToVideo, preferredResolution, preferredFrameRate } = individualStreamProperties; + const { scaleBehavior, subscribeToAudio, subscribeToVideo, preferredResolution, preferredFrameRate } = individualStreamProperties; + if (scaleBehavior !== undefined) { + OT.setSubscriberScaleBehavior(streamId, sanitizeScaleBehavior(scaleBehavior)); + } if (subscribeToAudio !== undefined) { OT.subscribeToAudio(streamId, sanitizeBooleanProperty(subscribeToAudio)); } diff --git a/src/helpers/OTPublisherHelper.js b/src/helpers/OTPublisherHelper.js index 7cb2d67d..16468fc0 100644 --- a/src/helpers/OTPublisherHelper.js +++ b/src/helpers/OTPublisherHelper.js @@ -33,9 +33,12 @@ const sanitizeVideoSource = (videoSource = 'camera') => (videoSource === 'camera const sanitizeAudioBitrate = (audioBitrate = 40000) => (audioBitrate < 80000 || audioBitrate > 128000 ? 40000 : audioBitrate); +const sanitizeScaleBehavior = (scaleBehavior = 'fill') => (scaleBehavior === 'fill' ? 'fill' : 'fit'); + const sanitizeProperties = (properties) => { if (typeof properties !== 'object') { return { + scaleBehavior: 'fill', videoTrack: true, audioTrack: true, publishAudio: true, @@ -50,6 +53,7 @@ const sanitizeProperties = (properties) => { }; } return { + scaleBehavior: sanitizeScaleBehavior(properties.scaleBehavior), videoTrack: sanitizeBooleanProperty(properties.videoTrack), audioTrack: sanitizeBooleanProperty(properties.audioTrack), publishAudio: sanitizeBooleanProperty(properties.publishAudio), @@ -86,6 +90,7 @@ const sanitizePublisherEvents = (publisherId, events) => { }; export { + sanitizeScaleBehavior, sanitizeProperties, sanitizePublisherEvents, }; diff --git a/src/helpers/OTSubscriberHelper.js b/src/helpers/OTSubscriberHelper.js index 52a9a001..e49adc65 100644 --- a/src/helpers/OTSubscriberHelper.js +++ b/src/helpers/OTSubscriberHelper.js @@ -48,7 +48,7 @@ const sanitizeSubscriberEvents = (events) => { const sanitizeResolution = (resolution) => { if ((typeof resolution !== 'object') || (resolution && resolution.width === void 0 && - resolution.height === void 0) || + resolution.height === void 0) || (resolution === null)) { return { width: MAX_SAFE_INTEGER, height: MAX_SAFE_INTEGER }; } @@ -89,9 +89,12 @@ const sanitizeFrameRate = (frameRate) => { } }; +const sanitizeScaleBehavior = (scaleBehavior = 'fill') => (scaleBehavior === 'fill' ? 'fill' : 'fit'); + const sanitizeProperties = (properties) => { if (typeof properties !== 'object') { return { + scaleBehavior: 'fill', subscribeToAudio: true, subscribeToVideo: true, preferredResolution: sanitizeResolution(null), @@ -99,6 +102,7 @@ const sanitizeProperties = (properties) => { }; } return { + scaleBehavior: sanitizeScaleBehavior(properties.scaleBehavior), subscribeToAudio: sanitizeBooleanProperty(properties.subscribeToAudio), subscribeToVideo: sanitizeBooleanProperty(properties.subscribeToVideo), preferredResolution: sanitizeResolution(properties.preferredResolution), @@ -107,6 +111,7 @@ const sanitizeProperties = (properties) => { }; export { + sanitizeScaleBehavior, sanitizeSubscriberEvents, sanitizeProperties, sanitizeFrameRate,