Merge branch 'mkirk/webrtc-mtl-view'

pull/1/head
Michael Kirk 8 years ago
commit 0989068822

@ -1 +1 @@
Subproject commit fc8eebb92367031f60ea337c897551cc54baa4b2
Subproject commit a3eede219e225d5ae420c3e46161a6363e45cefc

@ -1,7 +0,0 @@
The RTCAudioSession.h header isn't included in the standard build of
WebRTC, so we've vendored it here. Otherwise we're using the vanilla
framework.
We use the RTCAudioSession header to manually manage the RTC audio
session, so as to not start recording until the call is connected.

@ -1,224 +0,0 @@
/*
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#import "WebRTC/RTCMacros.h"
NS_ASSUME_NONNULL_BEGIN
extern NSString * const kRTCAudioSessionErrorDomain;
/** Method that requires lock was called without lock. */
extern NSInteger const kRTCAudioSessionErrorLockRequired;
/** Unknown configuration error occurred. */
extern NSInteger const kRTCAudioSessionErrorConfiguration;
@class RTCAudioSession;
@class RTCAudioSessionConfiguration;
// Surfaces AVAudioSession events. WebRTC will listen directly for notifications
// from AVAudioSession and handle them before calling these delegate methods,
// at which point applications can perform additional processing if required.
RTC_EXPORT
@protocol RTCAudioSessionDelegate <NSObject>
@optional
/** Called on a system notification thread when AVAudioSession starts an
* interruption event.
*/
- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session;
/** Called on a system notification thread when AVAudioSession ends an
* interruption event.
*/
- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session
shouldResumeSession:(BOOL)shouldResumeSession;
/** Called on a system notification thread when AVAudioSession changes the
* route.
*/
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
reason:(AVAudioSessionRouteChangeReason)reason
previousRoute:(AVAudioSessionRouteDescription *)previousRoute;
/** Called on a system notification thread when AVAudioSession media server
* terminates.
*/
- (void)audioSessionMediaServerTerminated:(RTCAudioSession *)session;
/** Called on a system notification thread when AVAudioSession media server
* restarts.
*/
- (void)audioSessionMediaServerReset:(RTCAudioSession *)session;
// TODO(tkchin): Maybe handle SilenceSecondaryAudioHintNotification.
- (void)audioSession:(RTCAudioSession *)session
didChangeCanPlayOrRecord:(BOOL)canPlayOrRecord;
/** Called on a WebRTC thread when the audio device is notified to begin
* playback or recording.
*/
- (void)audioSessionDidStartPlayOrRecord:(RTCAudioSession *)session;
/** Called on a WebRTC thread when the audio device is notified to stop
* playback or recording.
*/
- (void)audioSessionDidStopPlayOrRecord:(RTCAudioSession *)session;
@end
/** Proxy class for AVAudioSession that adds a locking mechanism similar to
* AVCaptureDevice. This is used to that interleaving configurations between
* WebRTC and the application layer are avoided.
*
* RTCAudioSession also coordinates activation so that the audio session is
* activated only once. See |setActive:error:|.
*/
RTC_EXPORT
@interface RTCAudioSession : NSObject
/** Convenience property to access the AVAudioSession singleton. Callers should
* not call setters on AVAudioSession directly, but other method invocations
* are fine.
*/
@property(nonatomic, readonly) AVAudioSession *session;
/** Our best guess at whether the session is active based on results of calls to
* AVAudioSession.
*/
@property(nonatomic, readonly) BOOL isActive;
/** Whether RTCAudioSession is currently locked for configuration. */
@property(nonatomic, readonly) BOOL isLocked;
/** If YES, WebRTC will not initialize the audio unit automatically when an
* audio track is ready for playout or recording. Instead, applications should
* call setIsAudioEnabled. If NO, WebRTC will initialize the audio unit
* as soon as an audio track is ready for playout or recording.
*/
@property(nonatomic, assign) BOOL useManualAudio;
/** This property is only effective if useManualAudio is YES.
* Represents permission for WebRTC to initialize the VoIP audio unit.
* When set to NO, if the VoIP audio unit used by WebRTC is active, it will be
* stopped and uninitialized. This will stop incoming and outgoing audio.
* When set to YES, WebRTC will initialize and start the audio unit when it is
* needed (e.g. due to establishing an audio connection).
* This property was introduced to work around an issue where if an AVPlayer is
* playing audio while the VoIP audio unit is initialized, its audio would be
* either cut off completely or played at a reduced volume. By preventing
* the audio unit from being initialized until after the audio has completed,
* we are able to prevent the abrupt cutoff.
*/
@property(nonatomic, assign) BOOL isAudioEnabled;
// Proxy properties.
@property(readonly) NSString *category;
@property(readonly) AVAudioSessionCategoryOptions categoryOptions;
@property(readonly) NSString *mode;
@property(readonly) BOOL secondaryAudioShouldBeSilencedHint;
@property(readonly) AVAudioSessionRouteDescription *currentRoute;
@property(readonly) NSInteger maximumInputNumberOfChannels;
@property(readonly) NSInteger maximumOutputNumberOfChannels;
@property(readonly) float inputGain;
@property(readonly) BOOL inputGainSettable;
@property(readonly) BOOL inputAvailable;
@property(readonly, nullable)
NSArray<AVAudioSessionDataSourceDescription *> * inputDataSources;
@property(readonly, nullable)
AVAudioSessionDataSourceDescription *inputDataSource;
@property(readonly, nullable)
NSArray<AVAudioSessionDataSourceDescription *> * outputDataSources;
@property(readonly, nullable)
AVAudioSessionDataSourceDescription *outputDataSource;
@property(readonly) double sampleRate;
@property(readonly) double preferredSampleRate;
@property(readonly) NSInteger inputNumberOfChannels;
@property(readonly) NSInteger outputNumberOfChannels;
@property(readonly) float outputVolume;
@property(readonly) NSTimeInterval inputLatency;
@property(readonly) NSTimeInterval outputLatency;
@property(readonly) NSTimeInterval IOBufferDuration;
@property(readonly) NSTimeInterval preferredIOBufferDuration;
/** Default constructor. */
+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
/** Adds a delegate, which is held weakly. */
- (void)addDelegate:(id<RTCAudioSessionDelegate>)delegate;
/** Removes an added delegate. */
- (void)removeDelegate:(id<RTCAudioSessionDelegate>)delegate;
/** Request exclusive access to the audio session for configuration. This call
* will block if the lock is held by another object.
*/
- (void)lockForConfiguration;
/** Relinquishes exclusive access to the audio session. */
- (void)unlockForConfiguration;
/** If |active|, activates the audio session if it isn't already active.
* Successful calls must be balanced with a setActive:NO when activation is no
* longer required. If not |active|, deactivates the audio session if one is
* active and this is the last balanced call. When deactivating, the
* AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation option is passed to
* AVAudioSession.
*/
- (BOOL)setActive:(BOOL)active
error:(NSError **)outError;
// The following methods are proxies for the associated methods on
// AVAudioSession. |lockForConfiguration| must be called before using them
// otherwise they will fail with kRTCAudioSessionErrorLockRequired.
- (BOOL)setCategory:(NSString *)category
withOptions:(AVAudioSessionCategoryOptions)options
error:(NSError **)outError;
- (BOOL)setMode:(NSString *)mode error:(NSError **)outError;
- (BOOL)setInputGain:(float)gain error:(NSError **)outError;
- (BOOL)setPreferredSampleRate:(double)sampleRate error:(NSError **)outError;
- (BOOL)setPreferredIOBufferDuration:(NSTimeInterval)duration
error:(NSError **)outError;
- (BOOL)setPreferredInputNumberOfChannels:(NSInteger)count
error:(NSError **)outError;
- (BOOL)setPreferredOutputNumberOfChannels:(NSInteger)count
error:(NSError **)outError;
- (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride
error:(NSError **)outError;
- (BOOL)setPreferredInput:(AVAudioSessionPortDescription *)inPort
error:(NSError **)outError;
- (BOOL)setInputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
error:(NSError **)outError;
- (BOOL)setOutputDataSource:(AVAudioSessionDataSourceDescription *)dataSource
error:(NSError **)outError;
@end
@interface RTCAudioSession (Configuration)
/** Applies the configuration to the current session. Attempts to set all
* properties even if previous ones fail. Only the last error will be
* returned.
* |lockForConfiguration| must be called first.
*/
- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration
error:(NSError **)outError;
/** Convenience method that calls both setConfiguration and setActive.
* |lockForConfiguration| must be called first.
*/
- (BOOL)setConfiguration:(RTCAudioSessionConfiguration *)configuration
active:(BOOL)active
error:(NSError **)outError;
@end
NS_ASSUME_NONNULL_END

@ -124,6 +124,7 @@
450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
4509E79A1DD653700025A59F /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4509E7991DD653700025A59F /* WebRTC.framework */; };
450D19131F85236600970622 /* RemoteVideoView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450D19121F85236600970622 /* RemoteVideoView.m */; };
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2041E0D74AC003D14BE /* Platform.swift */; };
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */; };
451686AB1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451686AA1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift */; };
@ -601,6 +602,8 @@
450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncomingMessageCollectionViewCell.m; sourceTree = "<group>"; };
450873C91D9D86F4006B54F2 /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = "<group>"; };
4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = Carthage/Build/iOS/WebRTC.framework; sourceTree = "<group>"; };
450D19111F85236600970622 /* RemoteVideoView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteVideoView.h; sourceTree = "<group>"; };
450D19121F85236600970622 /* RemoteVideoView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RemoteVideoView.m; sourceTree = "<group>"; };
450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = UserNotificationsAdaptee.swift; path = UserInterface/Notifications/UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
451686AA1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiDeviceProfileKeyUpdateJob.swift; sourceTree = "<group>"; };
@ -1526,6 +1529,8 @@
34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */,
34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */,
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */,
);
name = Views;
path = views;
@ -2259,6 +2264,7 @@
45CD81EF1DC030E7004C9430 /* AccountManager.swift in Sources */,
45794E861E00620000066731 /* CallUIAdapter.swift in Sources */,
4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */,
450D19131F85236600970622 /* RemoteVideoView.m in Sources */,
FCFA64B71A24F6730007FB87 /* UIFont+OWS.m in Sources */,
B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */,
45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */,

@ -32,6 +32,7 @@
#import "ProfileViewController.h"
#import "PushManager.h"
#import "Release.h"
#import "RemoteVideoView.h"
#import "TSMessageAdapter.h"
#import "ThreadUtil.h"
#import "UIColor+OWS.h"
@ -110,5 +111,4 @@
#import <SignalServiceKit/TSThread.h>
#import <WebRTC/RTCAudioSession.h>
#import <WebRTC/RTCCameraPreviewView.h>
#import <WebRTC/RTCEAGLVideoView.h>
#import <YYImage/YYImage.h>

@ -36,6 +36,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
- (void)autoPinHeightToHeightOfView:(UIView *)view;
- (NSLayoutConstraint *)autoPinToSquareAspectRatio;
- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio;
#pragma mark - Content Hugging and Compression Resistance

@ -100,14 +100,24 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
- (NSLayoutConstraint *)autoPinToSquareAspectRatio
{
self.translatesAutoresizingMaskIntoConstraints = NO;
return [self autoPinToAspectRatio:1.0];
}
- (NSLayoutConstraint *)autoPinToAspectRatio:(CGFloat)ratio
{
// Clamp to ensure view has reasonable aspect ratio.
CGFloat clampedRatio = Clamp(ratio, 0.5, 95.0);
if (clampedRatio != ratio) {
OWSFail(@"Invalid aspect ratio: %f for view: %@", ratio, self);
}
self.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeHeight
multiplier:1.f
multiplier:clampedRatio
constant:0.f];
[constraint autoInstall];
return constraint;

@ -8,7 +8,7 @@ import PromiseKit
// TODO: Add category so that button handlers can be defined where button is created.
// TODO: Ensure buttons enabled & disabled as necessary.
class CallViewController: OWSViewController, CallObserver, CallServiceObserver, RTCEAGLVideoViewDelegate {
class CallViewController: OWSViewController, CallObserver, CallServiceObserver {
let TAG = "[CallViewController]"
@ -60,12 +60,10 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
// MARK: Video Views
var remoteVideoView: RTCEAGLVideoView!
var remoteVideoView: RemoteVideoView!
var localVideoView: RTCCameraPreviewView!
weak var localVideoTrack: RTCVideoTrack?
weak var remoteVideoTrack: RTCVideoTrack?
var remoteVideoSize: CGSize! = CGSize.zero
var remoteVideoConstraints: [NSLayoutConstraint] = []
var localVideoConstraints: [NSLayoutConstraint] = []
var shouldRemoteVideoControlsBeHidden = false {
@ -232,10 +230,10 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
func createVideoViews() {
remoteVideoView = RTCEAGLVideoView()
remoteVideoView.delegate = self
remoteVideoView = RemoteVideoView()
remoteVideoView.isUserInteractionEnabled = false
localVideoView = RTCCameraPreviewView()
remoteVideoView.isHidden = true
localVideoView.isHidden = true
self.view.addSubview(remoteVideoView)
@ -548,6 +546,8 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
localVideoView.autoSetDimension(.width, toSize:localVideoSize)
localVideoView.autoSetDimension(.height, toSize:localVideoSize)
remoteVideoView.autoPinEdgesToSuperviewEdges()
contactNameLabel.autoPinEdge(toSuperviewEdge:.top, withInset:topMargin)
contactNameLabel.autoPinLeadingToSuperview(withMargin: contactHMargin)
contactNameLabel.setContentHuggingVerticalHigh()
@ -588,52 +588,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
internal func updateRemoteVideoLayout() {
NSLayoutConstraint.deactivate(self.remoteVideoConstraints)
var constraints: [NSLayoutConstraint] = []
// We fill the screen with the remote video. The remote video's
// aspect ratio may not (and in fact will very rarely) match the
// aspect ratio of the current device, so parts of the remote
// video will be hidden offscreen.
//
// It's better to trim the remote video than to adopt a letterboxed
// layout.
if remoteVideoSize.width > 0 && remoteVideoSize.height > 0 &&
self.view.bounds.size.width > 0 && self.view.bounds.size.height > 0 {
var remoteVideoWidth = self.view.bounds.size.width
var remoteVideoHeight = self.view.bounds.size.height
if remoteVideoSize.width / self.view.bounds.size.width > remoteVideoSize.height / self.view.bounds.size.height {
remoteVideoWidth = round(self.view.bounds.size.height * remoteVideoSize.width / remoteVideoSize.height)
} else {
remoteVideoHeight = round(self.view.bounds.size.width * remoteVideoSize.height / remoteVideoSize.width)
}
constraints.append(remoteVideoView.autoSetDimension(.width, toSize:remoteVideoWidth))
constraints.append(remoteVideoView.autoSetDimension(.height, toSize:remoteVideoHeight))
constraints += remoteVideoView.autoCenterInSuperview()
remoteVideoView.frame = CGRect(origin:CGPoint.zero,
size:CGSize(width:remoteVideoWidth,
height:remoteVideoHeight))
remoteVideoView.isHidden = false
} else {
constraints += remoteVideoView.autoPinEdgesToSuperviewEdges()
remoteVideoView.isHidden = true
}
self.remoteVideoConstraints = constraints
// We need to force relayout to occur immediately (and not
// wait for a UIKit layout/render pass) or the remoteVideoView
// (which presumably is updating its CALayer directly) will
// ocassionally appear to have bad frames.
remoteVideoView.setNeedsLayout()
remoteVideoView.superview?.setNeedsLayout()
remoteVideoView.layoutIfNeeded()
remoteVideoView.superview?.layoutIfNeeded()
remoteVideoView.isHidden = !self.hasRemoteVideoTrack
updateCallUI(callState: call.state)
}
@ -988,6 +943,10 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
updateLocalVideoLayout()
}
var hasRemoteVideoTrack: Bool {
return self.remoteVideoTrack != nil
}
internal func updateRemoteVideoTrack(remoteVideoTrack: RTCVideoTrack?) {
AssertIsOnMainThread()
guard self.remoteVideoTrack != remoteVideoTrack else {
@ -1001,10 +960,6 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
self.remoteVideoTrack?.add(remoteVideoView)
shouldRemoteVideoControlsBeHidden = false
if remoteVideoTrack == nil {
remoteVideoSize = CGSize.zero
}
updateRemoteVideoLayout()
}
@ -1064,22 +1019,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
remoteVideoTrack: RTCVideoTrack?) {
AssertIsOnMainThread()
updateLocalVideoTrack(localVideoTrack:localVideoTrack)
updateRemoteVideoTrack(remoteVideoTrack:remoteVideoTrack)
}
// MARK: - RTCEAGLVideoViewDelegate
internal func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
AssertIsOnMainThread()
if videoView != remoteVideoView {
return
}
Logger.info("\(TAG) \(#function): \(size)")
remoteVideoSize = size
updateRemoteVideoLayout()
updateLocalVideoTrack(localVideoTrack: localVideoTrack)
updateRemoteVideoTrack(remoteVideoTrack: remoteVideoTrack)
}
}

@ -417,8 +417,14 @@ struct AudioSource: Hashable {
let session = AVAudioSession.sharedInstance()
guard let availableInputs = session.availableInputs else {
// I'm not sure when this would happen.
owsFail("No available inputs or inputs not ready")
// I'm not sure why this would happen, but it may indicate an error.
// In practice, I haven't seen it on iOS9+.
//
// I *have* seen it on iOS8, but it doesn't seem to cause any problems,
// so we do *not* trigger the assert on that platform.
if #available(iOS 9.0, *) {
owsFail("No available inputs or inputs not ready")
}
return [AudioSource.builtInSpeaker]
}

@ -209,7 +209,7 @@ protocol CallServiceObserver: class {
didSet {
AssertIsOnMainThread()
Logger.info("\(self.TAG) \(#function)")
Logger.info("\(self.TAG) \(#function): \(isRemoteVideoEnabled)")
fireDidUpdateVideoTracks()
}
@ -1043,10 +1043,8 @@ protocol CallServiceObserver: class {
AssertIsOnMainThread()
guard let call = self.call else {
// This should never happen; return to a known good state.
owsFail("\(TAG) call was unexpectedly nil in \(#function)")
OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file:#file, function:#function, line:#line)
handleFailedCurrentCall(error: CallError.assertionError(description:"\(TAG) call unexpectedly nil in \(#function)"))
// This can happen after a call has ended. Reproducible on iOS11, when the other party ends the call.
Logger.info("\(TAG) ignoring mute request for obsolete call")
return
}

@ -0,0 +1,17 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <WebRTC/RTCVideoRenderer.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Drives the full screen remote video. This is *not* a swift class
* so we can take advantage of some compile time constants from WebRTC
*/
@interface RemoteVideoView : UIView <RTCVideoRenderer>
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,238 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "RemoteVideoView.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <MetalKit/MetalKit.h>
#import <PureLayout/PureLayout.h>
#import <SignalServiceKit/Threading.h>
#import <WebRTC/RTCEAGLVideoView.h>
#import <WebRTC/RTCMTLVideoView.h>
#import <WebRTC/RTCVideoRenderer.h>
NS_ASSUME_NONNULL_BEGIN
// As of RTC M61, iOS8 crashes when ending calls while de-alloc'ing the EAGLVideoView.
// WebRTC doesn't seem to support iOS8 - e.g. their Podfile requires iOS9+, and they
// unconditionally require MetalKit on a 64bit iOS8 device (which crashes).
// Until WebRTC supports iOS8, we show a "upgrade iOS to see remote video" view
// to our few remaining iOS8 users
@interface NullVideoRenderer : UIView <RTCVideoRenderer>
@end
@implementation NullVideoRenderer
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
self.backgroundColor = UIColor.blackColor;
UILabel *label = [UILabel new];
label.numberOfLines = 0;
label.text
= NSLocalizedString(@"CALL_REMOTE_VIDEO_DISABLED", @"Text shown on call screen in place of remote video");
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont ows_boldFontWithSize:ScaleFromIPhone5(20)];
label.textColor = UIColor.whiteColor;
label.lineBreakMode = NSLineBreakByWordWrapping;
[self addSubview:label];
[label autoVCenterInSuperview];
[label autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5(16)];
return self;
}
#pragma mark - RTCVideoRenderer
/** The size of the frame. */
- (void)setSize:(CGSize)size
{
// Do nothing.
}
/** The frame to be displayed. */
- (void)renderFrame:(nullable RTCVideoFrame *)frame
{
// Do nothing.
}
@end
@interface RemoteVideoView () <RTCEAGLVideoViewDelegate>
@property (nonatomic, readonly) __kindof UIView<RTCVideoRenderer> *videoRenderer;
// Used for legacy EAGLVideoView
@property (nullable, nonatomic) NSMutableArray<NSLayoutConstraint *> *remoteVideoConstraints;
@end
@implementation RemoteVideoView
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
// On iOS8: prints a message saying the feature is unavailable.
if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0)) {
_videoRenderer = [NullVideoRenderer new];
[self addSubview:_videoRenderer];
[_videoRenderer autoPinEdgesToSuperviewEdges];
}
// Currently RTC only supports metal on 64bit machines
#if defined(RTC_SUPPORTS_METAL)
// On 64-bit, iOS9+: uses the MetalKit backed view for improved battery/rendering performance.
if (_videoRenderer == nil) {
// It is insufficient to check the RTC_SUPPORTS_METAL macro to determine Metal support.
// RTCMTLVideoView requires the MTKView class, available only in iOS9+
// So check that it exists before proceeding.
if ([MTKView class]) {
_videoRenderer = [[RTCMTLVideoView alloc] initWithFrame:CGRectZero];
[self addSubview:_videoRenderer];
[_videoRenderer autoPinEdgesToSuperviewEdges];
// HACK: Although RTCMTLVideo view is positioned to the top edge of the screen
// It's inner (private) MTKView is below the status bar.
for (UIView *subview in [_videoRenderer subviews]) {
if ([subview isKindOfClass:[MTKView class]]) {
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired
forConstraints:^{
[subview autoPinEdgesToSuperviewEdges];
}];
} else {
OWSFail(@"New subviews added to MTLVideoView. Reconsider this hack.");
}
}
}
}
#elif defined(__arm64__)
// Canary incase the upstream RTC_SUPPORTS_METAL macro changes semantics
OWSFail(@"should only use legacy video view on 32bit systems");
#endif
// On 32-bit iOS9+ systems, use the legacy EAGL backed view.
if (_videoRenderer == nil) {
RTCEAGLVideoView *eaglVideoView = [RTCEAGLVideoView new];
eaglVideoView.delegate = self;
_videoRenderer = eaglVideoView;
[self addSubview:_videoRenderer];
// Pinning legacy RTCEAGL view discards aspect ratio.
// So we have a more verbose layout in the RTCEAGLVideoViewDelegate methods
// [_videoRenderer autoPinEdgesToSuperviewEdges];
}
// We want the rendered video to go edge-to-edge.
_videoRenderer.layoutMargins = UIEdgeInsetsZero;
return self;
}
#pragma mark - RTCVideoRenderer
/** The size of the frame. */
- (void)setSize:(CGSize)size
{
[self.videoRenderer setSize:size];
}
#pragma mark - RTCEAGLVideoViewDelegate
- (void)videoView:(RTCEAGLVideoView *)videoView didChangeVideoSize:(CGSize)remoteVideoSize
{
AssertIsOnMainThread();
if (remoteVideoSize.height <= 0) {
OWSFail(@"Illegal video height: %f", remoteVideoSize.height);
return;
}
CGFloat aspectRatio = remoteVideoSize.width / remoteVideoSize.height;
DDLogVerbose(@"%@ Remote video size: width: %f height: %f ratio: %f",
self.logTag,
remoteVideoSize.width,
remoteVideoSize.height,
aspectRatio);
UIView *containingView = self.superview;
if (containingView == nil) {
DDLogDebug(@"%@ Cannot layout video view without superview", self.logTag);
return;
}
if (![self.videoRenderer isKindOfClass:[RTCEAGLVideoView class]]) {
OWSFail(@"%@ Unexpected video renderer: %@", self.logTag, self.videoRenderer);
return;
}
[NSLayoutConstraint deactivateConstraints:self.remoteVideoConstraints];
NSMutableArray<NSLayoutConstraint *> *constraints = [NSMutableArray new];
if (remoteVideoSize.width > 0 && remoteVideoSize.height > 0 && containingView.bounds.size.width > 0
&& containingView.bounds.size.height > 0) {
// to approximate "scale to fill" contentMode
// - Pin aspect ratio
// - Width and height is *at least* as wide as superview
[constraints addObject:[videoView autoPinToAspectRatio:aspectRatio]];
[constraints addObject:[videoView autoSetDimension:ALDimensionWidth
toSize:containingView.width
relation:NSLayoutRelationGreaterThanOrEqual]];
[constraints addObject:[videoView autoSetDimension:ALDimensionHeight
toSize:containingView.height
relation:NSLayoutRelationGreaterThanOrEqual]];
[constraints addObjectsFromArray:[videoView autoCenterInSuperview]];
// Low priority constraints force view to be no larger than necessary.
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{
[constraints addObjectsFromArray:[videoView autoPinEdgesToSuperviewEdges]];
}];
} else {
[constraints addObjectsFromArray:[videoView autoPinEdgesToSuperviewEdges]];
}
self.remoteVideoConstraints = constraints;
// We need to force relayout to occur immediately (and not
// wait for a UIKit layout/render pass) or the remoteVideoView
// (which presumably is updating its CALayer directly) will
// ocassionally appear to have bad frames.
[videoView setNeedsLayout];
[[videoView superview] setNeedsLayout];
[videoView layoutIfNeeded];
[[videoView superview] layoutIfNeeded];
}
/** The frame to be displayed. */
- (void)renderFrame:(nullable RTCVideoFrame *)frame
{
[self.videoRenderer renderFrame:frame];
}
#pragma mark - Logging
+ (NSString *)logTag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)logTag
{
return self.class.logTag;
}
@end
NS_ASSUME_NONNULL_END

@ -208,6 +208,9 @@
/* Accessibilty label for placing call button */
"CALL_LABEL" = "Call";
/* Text shown on call screen in place of remote video */
"CALL_REMOTE_VIDEO_DISABLED" = "Please upgrade to iOS 9 or newer to see remote video.";
/* Call setup status label after outgoing call times out */
"CALL_SCREEN_STATUS_NO_ANSWER" = "No Answer.";
@ -601,7 +604,7 @@
/* A label for generic attachments. */
"GENERIC_ATTACHMENT_LABEL" = "Attachment";
/* Please enter your search. */
/* Alert message shown when user tries to search for GIFs without entering any search terms. */
"GIF_PICKER_VIEW_MISSING_QUERY" = "Please enter your search.";
/* Title for the 'gif picker' dialog. */

Loading…
Cancel
Save