diff --git a/Carthage b/Carthage index fc8eebb92..a3eede219 160000 --- a/Carthage +++ b/Carthage @@ -1 +1 @@ -Subproject commit fc8eebb92367031f60ea337c897551cc54baa4b2 +Subproject commit a3eede219e225d5ae420c3e46161a6363e45cefc diff --git a/Libraries/WebRTC/README.md b/Libraries/WebRTC/README.md deleted file mode 100644 index 119fded51..000000000 --- a/Libraries/WebRTC/README.md +++ /dev/null @@ -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. - diff --git a/Libraries/WebRTC/RTCAudioSession.h b/Libraries/WebRTC/RTCAudioSession.h deleted file mode 100644 index 199fd17b2..000000000 --- a/Libraries/WebRTC/RTCAudioSession.h +++ /dev/null @@ -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 -#import - -#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 - -@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 * inputDataSources; -@property(readonly, nullable) - AVAudioSessionDataSourceDescription *inputDataSource; -@property(readonly, nullable) - NSArray * 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)delegate; -/** Removes an added delegate. */ -- (void)removeDelegate:(id)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 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 387675b5a..943d0799d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 450873C91D9D86F4006B54F2 /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = ""; }; 4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = Carthage/Build/iOS/WebRTC.framework; sourceTree = ""; }; + 450D19111F85236600970622 /* RemoteVideoView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteVideoView.h; sourceTree = ""; }; + 450D19121F85236600970622 /* RemoteVideoView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RemoteVideoView.m; sourceTree = ""; }; 450DF2041E0D74AC003D14BE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = UserNotificationsAdaptee.swift; path = UserInterface/Notifications/UserNotificationsAdaptee.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 451686AA1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiDeviceProfileKeyUpdateJob.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 786e7205d..62b761453 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -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 #import #import -#import #import diff --git a/Signal/src/UIView+OWS.h b/Signal/src/UIView+OWS.h index 62ec64920..498d04421 100644 --- a/Signal/src/UIView+OWS.h +++ b/Signal/src/UIView+OWS.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 diff --git a/Signal/src/UIView+OWS.m b/Signal/src/UIView+OWS.m index e3bdbaaef..f8eaf9dad 100644 --- a/Signal/src/UIView+OWS.m +++ b/Signal/src/UIView+OWS.m @@ -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; diff --git a/Signal/src/ViewControllers/CallViewController.swift b/Signal/src/ViewControllers/CallViewController.swift index c3d91ca61..7873aa072 100644 --- a/Signal/src/ViewControllers/CallViewController.swift +++ b/Signal/src/ViewControllers/CallViewController.swift @@ -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) } } diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 4f93b457b..8f38afc40 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -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] } diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 4d82d99e0..40cc4c886 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -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 } diff --git a/Signal/src/views/RemoteVideoView.h b/Signal/src/views/RemoteVideoView.h new file mode 100644 index 000000000..353789890 --- /dev/null +++ b/Signal/src/views/RemoteVideoView.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +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 + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/views/RemoteVideoView.m b/Signal/src/views/RemoteVideoView.m new file mode 100644 index 000000000..39fc309d5 --- /dev/null +++ b/Signal/src/views/RemoteVideoView.m @@ -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 +#import +#import +#import +#import +#import + +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 + +@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 () + +@property (nonatomic, readonly) __kindof UIView *videoRenderer; + +// Used for legacy EAGLVideoView +@property (nullable, nonatomic) NSMutableArray *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 *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 diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 59137c75e..0a2dc6f9d 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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. */