diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index bb0dc499a..0d2797499 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -247,7 +247,6 @@ 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; }; 452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; }; 452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; }; - 453034AB200289F50018945D /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453034AA200289F50018945D /* VideoPlayerView.swift */; }; 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4535186A1FC635DD00210559 /* ShareViewController.swift */; }; 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; }; 453518721FC635DD00210559 /* SignalShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SignalShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -271,6 +270,7 @@ 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */; }; 45638BDC1F3DD0D400128435 /* DebugUICalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDB1F3DD0D400128435 /* DebugUICalling.swift */; }; 45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */; }; + 4565ED06200EA29900C46DBB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453034AA200289F50018945D /* VideoPlayerView.swift */; }; 45666F581D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */; }; 456C38961DC7B882007536A7 /* PromiseKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 451DE9F11DC1585F00810E42 /* PromiseKit.framework */; }; 456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */; }; @@ -1571,6 +1571,7 @@ 34D913491F62D4A500722898 /* SignalAttachment.swift */, 34CA1C281F7164F700E51C51 /* MediaMessageView.swift */, 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */, + 453034AA200289F50018945D /* VideoPlayerView.swift */, ); path = attachments; sourceTree = ""; @@ -1748,7 +1749,6 @@ 76EB052B18170B33006006FC /* Views */ = { isa = PBXGroup; children = ( - 453034AA200289F50018945D /* VideoPlayerView.swift */, 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */, 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */, 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */, @@ -2795,6 +2795,7 @@ 454A965A1FD6017E008D2A0E /* SignalAttachment.swift in Sources */, 454A965B1FD601BF008D2A0E /* MediaMessageView.swift in Sources */, 45BC829D1FD9C4B400011CF3 /* ShareViewDelegate.swift in Sources */, + 4565ED06200EA29900C46DBB /* VideoPlayerView.swift in Sources */, 3461295B1FD1D74C00532771 /* Environment.m in Sources */, 346129D51FD20ADC00532771 /* UIViewController+OWS.m in Sources */, 451F8A431FD714FE005CB9DA /* AvatarImageView.swift in Sources */, @@ -2925,7 +2926,6 @@ 34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */, 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */, 45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */, - 453034AB200289F50018945D /* VideoPlayerView.swift in Sources */, 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */, 34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */, 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, diff --git a/Signal/src/ViewControllers/MediaDetailViewController.m b/Signal/src/ViewControllers/MediaDetailViewController.m index c8e987615..84aff9dde 100644 --- a/Signal/src/ViewControllers/MediaDetailViewController.m +++ b/Signal/src/ViewControllers/MediaDetailViewController.m @@ -332,7 +332,6 @@ NS_ASSUME_NONNULL_BEGIN [playVideoButton autoCenterInSuperview]; } - // Don't show footer bar after tapping approval-view if (self.viewItem) { UIToolbar *footerBar = [UIToolbar new]; diff --git a/SignalMessaging/attachments/AttachmentApprovalViewController.swift b/SignalMessaging/attachments/AttachmentApprovalViewController.swift index 07cf998fe..e4ec9a7ae 100644 --- a/SignalMessaging/attachments/AttachmentApprovalViewController.swift +++ b/SignalMessaging/attachments/AttachmentApprovalViewController.swift @@ -3,6 +3,7 @@ // import Foundation +import AVFoundation import MediaPlayer @objc @@ -12,7 +13,7 @@ public protocol AttachmentApprovalViewControllerDelegate: class { } @objc -public class AttachmentApprovalViewController: OWSViewController, CaptioningToolbarDelegate { +public class AttachmentApprovalViewController: OWSViewController, CaptioningToolbarDelegate, PlayerProgressBarDelegate { let TAG = "[AttachmentApprovalViewController]" weak var delegate: AttachmentApprovalViewControllerDelegate? @@ -26,10 +27,13 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool // MARK: Properties let attachment: SignalAttachment + private var videoPlayer: AVPlayer? private(set) var bottomToolbar: UIView! private(set) var mediaMessageView: MediaMessageView! private(set) var scrollView: UIScrollView! + private(set) var contentContainer: UIView! + private(set) var playVideoButton: UIView? // MARK: Initializers @@ -97,10 +101,15 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) + // Anything that should be shrunk when user pops keyboard lives in the contentContainer. + let contentContainer = UIView() + self.contentContainer = contentContainer + view.addSubview(contentContainer) + contentContainer.autoPinEdgesToSuperviewEdges() + // Scroll View - used to zoom/pan on images and video scrollView = UIScrollView() - view.addSubview(scrollView) - + contentContainer.addSubview(scrollView) scrollView.delegate = self scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false @@ -139,23 +148,6 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool topGradient.autoSetDimension(.height, toSize: ScaleFromIPhone5(60)) } - // Hide the play button embedded in the MediaView and replace it with our own. - // This allows us to zoom in on the media view without zooming in on the button - if attachment.isVideo { - self.mediaMessageView.videoPlayButton?.isHidden = true - let playButton = UIButton() - playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "accessability label for button to start media playback") - playButton.setBackgroundImage(#imageLiteral(resourceName: "play_button"), for: .normal) - playButton.contentMode = .scaleAspectFit - - let playButtonWidth = ScaleFromIPhone5(70) - playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) - self.view.addSubview(playButton) - - playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) - playButton.autoCenterInSuperview() - } - // Top Toolbar let topToolbar = makeClearToolbar() @@ -173,6 +165,67 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool let captioningToolbar = CaptioningToolbar() captioningToolbar.captioningToolbarDelegate = self self.bottomToolbar = captioningToolbar + + // Hide the play button embedded in the MediaView and replace it with our own. + // This allows us to zoom in on the media view without zooming in on the button + if attachment.isVideo { + + if #available(iOS 9.0, *) { + guard let videoURL = attachment.dataUrl else { + owsFail("Missing videoURL") + return + } + + let player = AVPlayer(url: videoURL) + self.videoPlayer = player + + NotificationCenter.default.addObserver(self, + selector: #selector(playerItemDidPlayToCompletion(_:)), + name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object: player.currentItem) + + let playerView = VideoPlayerView() + playerView.player = player + self.mediaMessageView.addSubview(playerView) + playerView.autoPinEdgesToSuperviewEdges() + + let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))) + playerView.addGestureRecognizer(pauseGesture) + + let progressBar = PlayerProgressBar() + progressBar.player = player + progressBar.delegate = self + + // we don't want the progress bar to zoom during "pinch-to-zoom" + // but we do want it to shrink with the media content when the user + // pops the keyboard. + contentContainer.addSubview(progressBar) + + progressBar.autoPinEdge(.top, to: .bottom, of: topToolbar) + progressBar.autoPinWidthToSuperview() + progressBar.autoSetDimension(.height, toSize: 44) + } + + self.mediaMessageView.videoPlayButton?.isHidden = true + let playButton = UIButton() + self.playVideoButton = playButton + playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "accessability label for button to start media playback") + playButton.setBackgroundImage(#imageLiteral(resourceName: "play_button"), for: .normal) + playButton.contentMode = .scaleAspectFit + + let playButtonWidth = ScaleFromIPhone5(70) + playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) + self.contentContainer.addSubview(playButton) + + playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) + playButton.autoCenterInSuperview() + } + } + + @available(iOS 9, *) + public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { + assert(self.videoPlayer != nil) + self.pauseVideo() } override public var inputAccessoryView: UIView? { @@ -202,8 +255,7 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool @objc public func playButtonTapped() { - // FIXME - use built in AVPlayer controls like MediaDetailViewController -// mediaMessageView.playVideo() + self.playVideo() } func cancelPressed(sender: UIButton) { @@ -228,6 +280,120 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool Logger.info("Changed height: \(newHeight)") } + // MARK: Video + + private func playVideo() { + Logger.info("\(TAG) in \(#function)") + + if #available(iOS 9, *) { + guard let videoPlayer = self.videoPlayer else { + owsFail("\(TAG) video player was unexpectedly nil") + return + } + + guard let playVideoButton = self.playVideoButton else { + owsFail("\(TAG) playVideoButton was unexpectedly nil") + return + } + UIView.animate(withDuration: 0.1) { + playVideoButton.alpha = 0.0 + } + + guard let item = videoPlayer.currentItem else { + owsFail("\(TAG) video player item was unexpectedly nil") + return + } + + if item.currentTime() == item.duration { + // Rewind for repeated plays, but only if it previously played to end. + videoPlayer.seek(to: kCMTimeZero) + } + + videoPlayer.play() + } else { + self.playLegacyVideo() + } + } + + private func playLegacyVideo() { + if #available(iOS 9, *) { + owsFail("should only use legacy video on iOS8") + } + + guard let videoURL = self.attachment.dataUrl else { + owsFail("videoURL was unexpectedly nil") + return + } + + guard let playerVC = MPMoviePlayerViewController(contentURL: videoURL) else { + owsFail("failed to init legacy video player") + return + } + + self.present(playerVC, animated: true) + } + + @available(iOS 9, *) + private func pauseVideo() { + guard let videoPlayer = self.videoPlayer else { + owsFail("\(TAG) video player was unexpectedly nil") + return + } + + videoPlayer.pause() + guard let playVideoButton = self.playVideoButton else { + owsFail("\(TAG) playVideoButton was unexpectedly nil") + return + } + UIView.animate(withDuration: 0.1) { + playVideoButton.alpha = 1.0 + } + } + + @objc + private func playerItemDidPlayToCompletion(_ notification: Notification) { + guard let playVideoButton = self.playVideoButton else { + owsFail("\(TAG) playVideoButton was unexpectedly nil") + return + } + UIView.animate(withDuration: 0.1) { + playVideoButton.alpha = 1.0 + } + } + + @available(iOS 9.0, *) + public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) { + // [self.videoPlayer pause]; + guard let videoPlayer = self.videoPlayer else { + owsFail("\(TAG) video player was unexpectedly nil") + return + } + videoPlayer.pause() + } + + @available(iOS 9.0, *) + public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) { + guard let videoPlayer = self.videoPlayer else { + owsFail("\(TAG) video player was unexpectedly nil") + return + } + + videoPlayer.seek(to: time) + } + + @available(iOS 9.0, *) + public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) { + guard let videoPlayer = self.videoPlayer else { + owsFail("\(TAG) video player was unexpectedly nil") + return + } + + videoPlayer.seek(to: time) + if (shouldResumePlayback) { + videoPlayer.play() + } + } + // MARK: Helpers var isZoomable: Bool { @@ -252,9 +418,9 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool private func scaleAttachmentView(_ fit: AttachmentViewScale) { guard shouldAllowAttachmentViewResizing else { - if self.scrollView.transform != CGAffineTransform.identity { + if self.contentContainer.transform != CGAffineTransform.identity { UIView.animate(withDuration: 0.2) { - self.scrollView.transform = CGAffineTransform.identity + self.contentContainer.transform = CGAffineTransform.identity } } return @@ -263,7 +429,7 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool switch fit { case .fullsize: UIView.animate(withDuration: 0.2) { - self.scrollView.transform = CGAffineTransform.identity + self.contentContainer.transform = CGAffineTransform.identity } case .compact: UIView.animate(withDuration: 0.2) { @@ -277,7 +443,7 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool let heightDelta = originalHeight * (1 - kScaleFactor) let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2) - self.scrollView.transform = scale.concatenating(translate) + self.contentContainer.transform = scale.concatenating(translate) } } } diff --git a/SignalMessaging/attachments/MediaMessageView.swift b/SignalMessaging/attachments/MediaMessageView.swift index 7aeb3623f..c7750252e 100644 --- a/SignalMessaging/attachments/MediaMessageView.swift +++ b/SignalMessaging/attachments/MediaMessageView.swift @@ -32,9 +32,6 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { @objc public let attachment: SignalAttachment - @objc - public var videoPlayer: MPMoviePlayerController? - @objc public var audioPlayer: OWSAudioAttachmentPlayer? diff --git a/Signal/src/views/VideoPlayerView.swift b/SignalMessaging/attachments/VideoPlayerView.swift similarity index 99% rename from Signal/src/views/VideoPlayerView.swift rename to SignalMessaging/attachments/VideoPlayerView.swift index 54b0a4e0b..19070ba32 100644 --- a/Signal/src/views/VideoPlayerView.swift +++ b/SignalMessaging/attachments/VideoPlayerView.swift @@ -3,6 +3,7 @@ // import Foundation +import AVFoundation @available(iOS 9.0, *) @objc