Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add scaleBehavior for subscriber and publisher view #474

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions @types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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.
*/
Expand Down Expand Up @@ -342,6 +347,11 @@ declare module "opentok-react-native" {
}

interface OTSubscriberProperties {
/**
* Subscriber view scale behavior. Defaults to "fill".
*/
scaleBehavior?: "fill" | "fit";

/**
* Whether to subscribe to audio.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")) {
Expand Down
32 changes: 30 additions & 2 deletions android/src/main/java/com/opentokreactnative/OTSessionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -77,7 +78,7 @@ public void initSession(String apiKey, String sessionId, ReadableMap sessionOpti
final boolean isCamera2Capable = sessionOptions.getBoolean("isCamera2Capable");
final boolean connectionEventsSuppressed = sessionOptions.getBoolean("connectionEventsSuppressed");
final boolean ipWhitelist = sessionOptions.getBoolean("ipWhitelist");
// Note: IceConfig is an additional property not supported at the moment.
// Note: IceConfig is an additional property not supported at the moment.
// final ReadableMap iceConfig = sessionOptions.getMap("iceConfig");
// final List<Session.Builder.IceServer> iceConfigServerList = (List<Session.Builder.IceServer>) iceConfig.getArray("customServers");
// final Session.Builder.IncludeServers iceConfigServerConfig; // = iceConfig.getString("includeServers");
Expand All @@ -102,7 +103,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)
Expand Down Expand Up @@ -145,6 +146,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")) {
Expand Down Expand Up @@ -178,6 +180,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<String, Publisher> mPublishers = sharedState.getPublishers();
mPublishers.put(publisherId, mPublisher);
callback.invoke();
Expand Down Expand Up @@ -220,6 +224,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"));
}
Expand Down Expand Up @@ -299,6 +305,28 @@ public void publishVideo(String publisherId, Boolean publishVideo) {
}
}

@ReactMethod
public void setPublisherScaleBehavior(String publisherId, String scaleBehavior) {

ConcurrentHashMap<String, Publisher> 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<String, Subscriber> 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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion docs/OTPublisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
* **streamDestroyed** (Object) - Sent when the publisher stops streaming.
2 changes: 2 additions & 0 deletions docs/OTSubscriber.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions ios/OpenTokReactNative/OTSessionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions ios/OpenTokReactNative/OTSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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()]);
}
}
Expand Down Expand Up @@ -146,6 +147,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 {
Expand Down Expand Up @@ -195,6 +197,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;
Expand Down
5 changes: 5 additions & 0 deletions ios/OpenTokReactNative/Utils/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions src/OTPublisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand Down
7 changes: 5 additions & 2 deletions src/OTSubscriber.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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));
}
Expand Down
5 changes: 5 additions & 0 deletions src/helpers/OTPublisherHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -50,6 +53,7 @@ const sanitizeProperties = (properties) => {
};
}
return {
scaleBehavior: sanitizeScaleBehavior(properties.scaleBehavior),
videoTrack: sanitizeBooleanProperty(properties.videoTrack),
audioTrack: sanitizeBooleanProperty(properties.audioTrack),
publishAudio: sanitizeBooleanProperty(properties.publishAudio),
Expand Down Expand Up @@ -86,6 +90,7 @@ const sanitizePublisherEvents = (publisherId, events) => {
};

export {
sanitizeScaleBehavior,
sanitizeProperties,
sanitizePublisherEvents,
};
7 changes: 6 additions & 1 deletion src/helpers/OTSubscriberHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -89,16 +89,20 @@ const sanitizeFrameRate = (frameRate) => {
}
};

const sanitizeScaleBehavior = (scaleBehavior = 'fill') => (scaleBehavior === 'fill' ? 'fill' : 'fit');

const sanitizeProperties = (properties) => {
if (typeof properties !== 'object') {
return {
scaleBehavior: 'fill',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having this as the default breaks it. I couldn't get it to work unless I changed this value here. I assume this is because the subscriber loads in a bit after the view is rendered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On which platform / or how does it break for you? 😯

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both. The subscriber scaleBehavior is always fill

Copy link
Contributor

@abdulajet abdulajet May 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscriber views, bottom one, should be fit

Screenshot 2021-05-27 at 11 54 07
Screenshot 2021-05-27 at 11 56 58

subscribeToAudio: true,
subscribeToVideo: true,
preferredResolution: sanitizeResolution(null),
preferredFrameRate: sanitizeFrameRate(null)
};
}
return {
scaleBehavior: sanitizeScaleBehavior(properties.scaleBehavior),
subscribeToAudio: sanitizeBooleanProperty(properties.subscribeToAudio),
subscribeToVideo: sanitizeBooleanProperty(properties.subscribeToVideo),
preferredResolution: sanitizeResolution(properties.preferredResolution),
Expand All @@ -107,6 +111,7 @@ const sanitizeProperties = (properties) => {
};

export {
sanitizeScaleBehavior,
sanitizeSubscriberEvents,
sanitizeProperties,
sanitizeFrameRate,
Expand Down