Videos play in full-screen media view controller, use modern movie

player.

// FREEBIE
pull/1/head
Michael Kirk 7 years ago
parent 81268012e5
commit 918e3f7dfe

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -18,7 +18,9 @@ NS_ASSUME_NONNULL_BEGIN
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem - (void)didTapImageViewItem:(ConversationViewItem *)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView; imageView:(UIView *)imageView;
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream; - (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView;
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream; - (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
- (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem; - (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem;
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem - (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "OWSMessageCell.h" #import "OWSMessageCell.h"
@ -1172,7 +1172,9 @@ NS_ASSUME_NONNULL_BEGIN
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
return; return;
case OWSMessageCellType_Video: case OWSMessageCellType_Video:
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream]; [self.delegate didTapVideoViewItem:self.viewItem
attachmentStream:self.attachmentStream
imageView:self.stillImageView];
return; return;
case OWSMessageCellType_GenericAttachment: case OWSMessageCellType_GenericAttachment:
[AttachmentSharing showShareUIForAttachment:self.attachmentStream]; [AttachmentSharing showShareUIForAttachment:self.attachmentStream];

@ -169,7 +169,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems; @property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache; @property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache;
@property (nonatomic, nullable) MPMoviePlayerController *videoPlayer;
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder; @property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
@property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer; @property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
@property (nonatomic, nullable) NSUUID *voiceMessageUUID; @property (nonatomic, nullable) NSUUID *voiceMessageUUID;
@ -1999,6 +1998,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
OWSAssert(attachmentStream); OWSAssert(attachmentStream);
OWSAssert(imageView); OWSAssert(imageView);
[self dismissKeyBoard];
UIWindow *window = [UIApplication sharedApplication].keyWindow; UIWindow *window = [UIApplication sharedApplication].keyWindow;
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window]; CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream
@ -2007,43 +2008,22 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[vc presentFromViewController:self]; [vc presentFromViewController:self];
} }
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream - (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIImageView *)imageView
{ {
OWSAssert([NSThread isMainThread]); OWSAssert([NSThread isMainThread]);
OWSAssert(viewItem); OWSAssert(viewItem);
OWSAssert(attachmentStream); OWSAssert(attachmentStream);
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) {
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL);
}
[self dismissKeyBoard]; [self dismissKeyBoard];
self.videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attachmentStream.mediaURL]; UIWindow *window = [UIApplication sharedApplication].keyWindow;
[_videoPlayer prepareToPlay]; CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerWillExitFullscreen:)
name:MPMoviePlayerWillExitFullscreenNotification
object:_videoPlayer];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerDidExitFullscreen:)
name:MPMoviePlayerDidExitFullscreenNotification
object:_videoPlayer];
_videoPlayer.controlStyle = MPMovieControlStyleDefault;
_videoPlayer.shouldAutoplay = YES;
[self.view addSubview:_videoPlayer.view];
// We can't animate from the cell media frame;
// MPMoviePlayerController will animate a crop of its
// contents rather than scaling them.
_videoPlayer.view.frame = self.view.bounds;
// FIXME inputAccessoryView - we lose and regain first responder here, causing keyboard to appear above video FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream
// Approaches: fromRect:convertedRect
// - put the video player in a separate VC (like the full image view controller) viewItem:viewItem];
// - some kind of "showing video" flag to supress first responder. [vc presentFromViewController:self];
[_videoPlayer setFullscreen:YES animated:NO];
} }
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream - (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
@ -2124,42 +2104,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self.navigationController pushViewController:view animated:YES]; [self.navigationController pushViewController:view animated:YES];
} }
#pragma mark - Video Playback
// There's more than one way to exit the fullscreen video playback.
// There's a done button, a "toggle fullscreen" button and I think
// there's some gestures too. These fire slightly different notifications.
// We want to hide & clean up the video player immediately in all of
// these cases.
- (void)moviePlayerWillExitFullscreen:(id)sender
{
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[self clearVideoPlayer];
}
// See comment on moviePlayerWillExitFullscreen:
- (void)moviePlayerDidExitFullscreen:(id)sender
{
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[self clearVideoPlayer];
}
- (void)clearVideoPlayer
{
[_videoPlayer stop];
[_videoPlayer.view removeFromSuperview];
self.videoPlayer = nil;
}
- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer
{
_videoPlayer = videoPlayer;
[ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil];
}
#pragma mark - System Messages #pragma mark - System Messages
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction - (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "FullImageViewController.h" #import "FullImageViewController.h"
@ -11,6 +11,8 @@
#import "UIColor+OWS.h" #import "UIColor+OWS.h"
#import "UIUtil.h" #import "UIUtil.h"
#import "UIView+OWS.h" #import "UIView+OWS.h"
#import <AVKit/AVKit.h>
#import <MediaPlayer/MediaPlayer.h>
#import <SignalServiceKit/NSData+Image.h> #import <SignalServiceKit/NSData+Image.h>
#import <YYImage/YYImage.h> #import <YYImage/YYImage.h>
@ -19,8 +21,6 @@ NS_ASSUME_NONNULL_BEGIN
#define kMinZoomScale 1.0f #define kMinZoomScale 1.0f
#define kMaxZoomScale 8.0f #define kMaxZoomScale 8.0f
#define kBackgroundAlpha 0.6f
// In order to use UIMenuController, the view from which it is // In order to use UIMenuController, the view from which it is
// presented must have certain custom behaviors. // presented must have certain custom behaviors.
@interface AttachmentMenuView : UIView @interface AttachmentMenuView : UIView
@ -47,14 +47,12 @@ NS_ASSUME_NONNULL_BEGIN
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate> @interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
@property (nonatomic) UIView *backgroundView;
@property (nonatomic) UIScrollView *scrollView; @property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) UIImageView *imageView; @property (nonatomic) UIImageView *imageView;
@property (nonatomic) UIButton *shareButton; @property (nonatomic) UIButton *shareButton;
@property (nonatomic) UIView *contentView;
@property (nonatomic) CGRect originRect; @property (nonatomic) CGRect originRect;
@property (nonatomic) BOOL isPresenting;
@property (nonatomic) NSData *fileData; @property (nonatomic) NSData *fileData;
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@ -62,6 +60,15 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) ConversationViewItem *viewItem; @property (nonatomic, nullable) ConversationViewItem *viewItem;
@property (nonatomic) UIToolbar *footerBar; @property (nonatomic) UIToolbar *footerBar;
@property (nonatomic) BOOL areToolbarsHidden;
@property (nonatomic, nullable) MPMoviePlayerController *mpVideoPlayer;
@property (nonatomic, nullable) AVPlayer *videoPlayer;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *imageViewConstraints;
@property (nonatomic, nullable) NSLayoutConstraint *imageViewBottomConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *imageViewLeadingConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *imageViewTopConstraint;
@property (nonatomic, nullable) NSLayoutConstraint *imageViewTrailingConstraint;
@end @end
@ -71,7 +78,6 @@ NS_ASSUME_NONNULL_BEGIN
fromRect:(CGRect)rect fromRect:(CGRect)rect
viewItem:(ConversationViewItem *_Nullable)viewItem viewItem:(ConversationViewItem *_Nullable)viewItem
{ {
self = [super initWithNibName:nil bundle:nil]; self = [super initWithNibName:nil bundle:nil];
if (self) { if (self) {
@ -85,7 +91,6 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect - (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect
{ {
self = [super initWithNibName:nil bundle:nil]; self = [super initWithNibName:nil bundle:nil];
if (self) { if (self) {
@ -139,22 +144,42 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
- (void)loadView { - (BOOL)isVideo
{
if (self.attachmentStream) {
return self.attachmentStream.isVideo;
} else if (self.attachment) {
return self.attachment.isVideo;
} else {
return NO;
}
}
- (void)loadView
{
self.view = [AttachmentMenuView new]; self.view = [AttachmentMenuView new];
self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha]; self.view.backgroundColor = [UIColor clearColor];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
} }
- (void)viewDidLoad { - (void)viewDidLoad
{
[super viewDidLoad]; [super viewDidLoad];
[self initializeBackground]; [self createContents];
[self initializeContentViewAndFooterBar];
[self initializeScrollView];
[self initializeImageView];
[self initializeGestureRecognizers]; [self initializeGestureRecognizers];
[self populateImageView:self.image]; // Even though bars are opaque, we want content to be layed out behind them.
// The bars might obscure part of the content, but they can easily be hidden by tapping
// The alternative would be that content would shift when the navbars hide.
self.extendedLayoutIncludesOpaqueBars = YES;
// TODO better title.
self.title = @"Attachment";
self.navigationItem.leftBarButtonItem =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
target:self
action:@selector(didTapDismissButton:)];
} }
- (void)viewWillDisappear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated {
@ -166,101 +191,281 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
#pragma mark - Initializers - (void)viewDidLayoutSubviews
{
- (void)initializeBackground { [super viewDidLayoutSubviews];
self.imageView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha]; [self updateMinZoomScale];
[self centerImageViewConstraints];
self.backgroundView = [UIView new];
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
[self.view addSubview:self.backgroundView];
[self.backgroundView autoPinEdgesToSuperviewEdges];
} }
- (void)initializeContentViewAndFooterBar { - (void)updateMinZoomScale
self.contentView = [UIView new]; {
[self.backgroundView addSubview:self.contentView]; CGSize viewSize = self.scrollView.bounds.size;
[self.contentView autoPinWidthToSuperview]; UIImage *image = self.imageView.image;
[self.contentView autoPinToTopLayoutGuideOfViewController:self withInset:0]; OWSAssert(image);
self.footerBar = [UIToolbar new];
_footerBar.barTintColor = [UIColor ows_signalBrandBlueColor];
[self.footerBar setItems:@[
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
target:self
action:@selector(shareWasPressed:)],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil],
]
animated:NO];
[self.backgroundView addSubview:self.footerBar];
[self.footerBar autoPinWidthToSuperview];
[self.footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0];
[self.footerBar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentView];
}
- (void)shareWasPressed:(id)sender {
DDLogInfo(@"%@: sharing image.", self.logTag);
[AttachmentSharing showShareUIForURL:self.attachmentUrl]; if (image.size.width == 0 || image.size.height == 0) {
} OWSFail(@"%@ Invalid image dimensions. %@", self.logTag, NSStringFromCGSize(image.size));
return;
}
- (void)initializeScrollView { CGFloat scaleWidth = viewSize.width / image.size.width;
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; CGFloat scaleHeight = viewSize.height / image.size.height;
self.scrollView.delegate = self; CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.scrollView.zoomScale = 1.0f; self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = kMaxZoomScale; self.scrollView.zoomScale = minScale;
self.scrollView.scrollEnabled = NO;
[self.contentView addSubview:self.scrollView];
} }
- (void)initializeImageView { #pragma mark - Initializers
- (void)createContents
{
CGFloat kFooterHeight = 44;
UIScrollView *scrollView = [UIScrollView new];
[self.view addSubview:scrollView];
self.scrollView = scrollView;
scrollView.delegate = self;
// TODO set max based on MIN.
scrollView.maximumZoomScale = kMaxZoomScale;
scrollView.showsVerticalScrollIndicator = NO;
scrollView.showsHorizontalScrollIndicator = NO;
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
self.automaticallyAdjustsScrollViewInsets = NO;
[scrollView autoPinToSuperviewEdges];
if (self.isAnimated) { if (self.isAnimated) {
if ([self.fileData ows_isValidImage]) { if ([self.fileData ows_isValidImage]) {
YYImage *animatedGif = [YYImage imageWithData:self.fileData]; YYImage *animatedGif = [YYImage imageWithData:self.fileData];
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] init]; YYAnimatedImageView *animatedView = [[YYAnimatedImageView alloc] init];
imageView.image = animatedGif; animatedView.image = animatedGif;
imageView.frame = self.originRect; self.imageView = animatedView;
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
self.imageView = imageView;
} else { } else {
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; self.imageView = [UIImageView new];
} }
} else if (self.isVideo) {
[self setupVideoPlayer];
// Present the static video preview
UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
self.imageView = imageView;
} else { } else {
// Present the static image using standard UIImageView // Present the static image using standard UIImageView
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.userInteractionEnabled = YES; self.imageView = imageView;
self.imageView.clipsToBounds = YES; }
self.imageView.layer.allowsEdgeAntialiasing = YES;
// Use trilinear filters for better scaling quality at OWSAssert(self.imageView);
// some performance cost.
self.imageView.layer.minificationFilter = kCAFilterTrilinear; [scrollView addSubview:self.imageView];
self.imageView.layer.magnificationFilter = kCAFilterTrilinear; self.imageView.contentMode = UIViewContentModeScaleAspectFit;
self.imageView.userInteractionEnabled = YES;
self.imageView.clipsToBounds = YES;
self.imageView.layer.allowsEdgeAntialiasing = YES;
self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
// Use trilinear filters for better scaling quality at
// some performance cost.
self.imageView.layer.minificationFilter = kCAFilterTrilinear;
self.imageView.layer.magnificationFilter = kCAFilterTrilinear;
[self applyInitialImageViewConstraints];
if (self.isVideo) {
UIButton *playButton = [UIButton new];
[playButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside];
UIImage *playImage = [UIImage imageNamed:@"play_button"];
[playButton setBackgroundImage:playImage forState:UIControlStateNormal];
playButton.contentMode = UIViewContentModeScaleAspectFill;
[self.view addSubview:playButton];
CGFloat playButtonWidth = ScaleFromIPhone5(70);
[playButton autoSetDimensionsToSize:CGSizeMake(playButtonWidth, playButtonWidth)];
[playButton autoCenterInSuperview];
} }
[self.scrollView addSubview:self.imageView]; UIToolbar *footerBar = [UIToolbar new];
_footerBar = footerBar;
footerBar.barTintColor = [UIColor ows_signalBrandBlueColor];
[footerBar setItems:@[
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
target:self
action:@selector(didPressShare:)],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil],
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
target:self
action:@selector(didPressDelete:)],
]
animated:NO];
[self.view addSubview:footerBar];
[footerBar autoPinWidthToSuperview];
[footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0];
[footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight];
}
- (void)applyInitialImageViewConstraints
{
if (self.imageViewConstraints.count > 0) {
[NSLayoutConstraint deactivateConstraints:self.imageViewConstraints];
}
CGRect convertedRect =
[self.imageView.superview convertRect:self.originRect fromView:[UIApplication sharedApplication].keyWindow];
NSMutableArray<NSLayoutConstraint *> *imageViewConstraints = [NSMutableArray new];
self.imageViewConstraints = imageViewConstraints;
[imageViewConstraints addObjectsFromArray:[self.imageView autoSetDimensionsToSize:convertedRect.size]];
[imageViewConstraints addObjectsFromArray:@[
[self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y],
[self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x]
]];
} }
- (void)populateImageView:(UIImage *)image { - (void)applyFinalImageViewConstraints
if (image && !self.isAnimated) { {
self.imageView.image = image; if (self.imageViewConstraints.count > 0) {
[NSLayoutConstraint deactivateConstraints:self.imageViewConstraints];
} }
NSMutableArray<NSLayoutConstraint *> *imageViewConstraints = [NSMutableArray new];
self.imageViewConstraints = imageViewConstraints;
self.imageViewLeadingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
self.imageViewTopConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTop];
self.imageViewTrailingConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
self.imageViewBottomConstraint = [self.imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[imageViewConstraints addObjectsFromArray:@[
self.imageViewTopConstraint,
self.imageViewTrailingConstraint,
self.imageViewBottomConstraint,
self.imageViewLeadingConstraint
]];
} }
- (void)initializeGestureRecognizers { - (void)setupVideoPlayer
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self {
action:@selector(imageDismissGesture:)]; NSFileManager *fileManager = [NSFileManager defaultManager];
singleTap.delegate = self; if (![fileManager fileExistsAtPath:[self.attachmentUrl path]]) {
[self.view addGestureRecognizer:singleTap]; OWSFail(@"%@ Missing video file: %@", self.logTag, self.attachmentStream.mediaURL);
}
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(imageDismissGesture:)]; if (@available(iOS 9.0, *)) {
AVPlayer *player = [[AVPlayer alloc] initWithURL:self.attachmentUrl];
self.videoPlayer = player;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidPlayToCompletion:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:player.currentItem];
} else {
MPMoviePlayerController *videoPlayer =
[[MPMoviePlayerController alloc] initWithContentURL:self.attachmentStream.mediaURL];
self.mpVideoPlayer = videoPlayer;
videoPlayer.controlStyle = MPMovieControlStyleNone;
[videoPlayer prepareToPlay];
//
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerWillExitFullscreen:)
// name:MPMoviePlayerWillExitFullscreenNotification
// object:videoPlayer];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerDidExitFullscreen:)
// name:MPMoviePlayerDidExitFullscreenNotification
// object:videoPlayer];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerWillEnterFullscreen:)
// name:MPMoviePlayerWillEnterFullscreenNotification
// object:videoPlayer];
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerPlaybackStateDidChange:)
// name:MPMoviePlayerPlaybackStateDidChangeNotification
// object:videoPlayer];
//
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerDidEnterFullscreen:)
// name:MPMoviePlayerDidEnterFullscreenNotification
// object:videoPlayer];
//
//
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:@selector(moviePlayerDidFinishPlayback:)
// name:MPMoviePlayerPlaybackDidFinishNotification
// object:videoPlayer];
//
// // Don't show any controls intially. We switch control style after the view is fullscreen to make them
// appear upon tapping.
//// videoPlayer.controlStyle = MPMovieControlStyleFullscreen;
// videoPlayer.shouldAutoplay = YES;
//
// // We can't animate from the cell media frame;
// // MPMoviePlayerController will animate a crop of its
// // contents rather than scaling them.
// videoPlayer.view.frame = self.view.bounds;
//
// self.imageView = videoPlayer.view;
}
}
- (void)setAreToolbarsHidden:(BOOL)areToolbarsHidden
{
if (_areToolbarsHidden == areToolbarsHidden) {
return;
}
_areToolbarsHidden = areToolbarsHidden;
if (!areToolbarsHidden) {
// Hiding the status bar affects the positioing of the navbar. We don't want to show that in the animation
// so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last.
[[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationFade];
}
[UIView animateWithDuration:0.1
animations:^(void) {
self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor;
self.navigationController.navigationBar.alpha = areToolbarsHidden ? 0 : 1;
self.footerBar.alpha = areToolbarsHidden ? 0 : 1;
}
completion:^(BOOL finished) {
// although navbar has 0 alpha at this point, if we don't also "hide" it, adjusting the status bar
// resets the alpha.
if (areToolbarsHidden) {
// [self.navigationController setNavigationBarHidden:areToolbarsHidden
// animated:NO];
// Hiding the status bar affects the positioing of the navbar. We don't want to show that in the
// animation so when *showing* the toolbars, we show the status bar first. When hiding, we hide it last.
[[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden
withAnimation:UIStatusBarAnimationNone];
// position the navbar, but have it be transparent
self.navigationController.navigationBar.alpha = 0;
}
}];
}
- (void)initializeGestureRecognizers
{
UITapGestureRecognizer *doubleTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapImage:)];
doubleTap.numberOfTapsRequired = 2; doubleTap.numberOfTapsRequired = 2;
doubleTap.delegate = self;
[self.view addGestureRecognizer:doubleTap]; [self.view addGestureRecognizer:doubleTap];
UITapGestureRecognizer *singleTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapImage:)];
[singleTap requireGestureRecognizerToFail:doubleTap];
[self.view addGestureRecognizer:singleTap];
// UISwipeGestureRecognizer supposedly supports multiple directions, // UISwipeGestureRecognizer supposedly supports multiple directions,
// but in practice it works better if you use a separate GR for each // but in practice it works better if you use a separate GR for each
// direction. // direction.
@ -270,8 +475,8 @@ NS_ASSUME_NONNULL_BEGIN
@(UISwipeGestureRecognizerDirectionUp), @(UISwipeGestureRecognizerDirectionUp),
@(UISwipeGestureRecognizerDirectionDown), @(UISwipeGestureRecognizerDirectionDown),
]) { ]) {
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self UISwipeGestureRecognizer *swipe =
action:@selector(imageDismissGesture:)]; [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(didSwipeImage:)];
swipe.direction = (UISwipeGestureRecognizerDirection) direction.integerValue; swipe.direction = (UISwipeGestureRecognizerDirection) direction.integerValue;
swipe.delegate = self; swipe.delegate = self;
[self.view addGestureRecognizer:swipe]; [self.view addGestureRecognizer:swipe];
@ -285,10 +490,39 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Gesture Recognizers #pragma mark - Gesture Recognizers
- (void)imageDismissGesture:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) { - (void)didTapDismissButton:(id)sender
{
[self dismiss];
}
- (void)didTapImage:(id)sender
{
DDLogVerbose(@"%@ did tap image.", self.logTag);
self.areToolbarsHidden = !self.areToolbarsHidden;
}
- (void)didDoubleTapImage:(id)sender
{
DDLogVerbose(@"%@ did tap image.", self.logTag);
if (self.scrollView.zoomScale == self.scrollView.minimumZoomScale) {
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale * 2 animated:YES];
} else {
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
}
}
- (void)didSwipeImage:(UIGestureRecognizer *)sender
{
// Ignore if image is zoomed in at all.
// e.g. otherwise, for example, if the image is horizontally larger than the scroll
// view, but fits vertically, swiping left/right will scroll the image, but swiping up/down
// would dismiss the image. That would not be intuitive.
if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) {
return;
}
[self dismiss]; [self dismiss];
}
} }
- (void)longPressGesture:(UIGestureRecognizer *)sender { - (void)longPressGesture:(UIGestureRecognizer *)sender {
@ -318,6 +552,31 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
- (void)didPressShare:(id)sender
{
DDLogInfo(@"%@: sharing image.", self.logTag);
[self.viewItem shareAction];
}
- (void)didPressDelete:(id)sender
{
DDLogInfo(@"%@: sharing image.", self.logTag);
UIAlertController *actionSheet =
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
[self.viewItem deleteAction];
[self dismiss];
}]];
[actionSheet addAction:[OWSAlerts cancelAction]];
[self presentViewController:actionSheet animated:YES completion:nil];
}
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
{ {
if (action == self.viewItem.metadataActionSelector) { if (action == self.viewItem.metadataActionSelector) {
@ -343,200 +602,247 @@ NS_ASSUME_NONNULL_BEGIN
- (void)deleteAction:(nullable id)sender - (void)deleteAction:(nullable id)sender
{ {
[self.viewItem deleteAction]; [self didPressDelete:sender];
[self dismiss];
} }
- (BOOL)canBecomeFirstResponder #pragma mark - Presentation
- (void)presentFromViewController:(UIViewController *)viewController
{ {
return YES; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self];
}
#pragma mark - Presentation // UIModalPresentationCustom retains the current view context behind our VC, allowing us to manually
// animate in our view, over the existing context, similar to a cross disolve, but allowing us to have
// more fine grained control
navController.modalPresentationStyle = UIModalPresentationCustom;
navController.navigationBar.barTintColor = UIColor.ows_materialBlueColor;
navController.navigationBar.translucent = NO;
navController.navigationBar.opaque = YES;
- (void)presentFromViewController:(UIViewController *)viewController {
_isPresenting = YES;
self.view.userInteractionEnabled = NO; self.view.userInteractionEnabled = NO;
[self.view addSubview:self.imageView];
self.modalPresentationStyle = UIModalPresentationOverCurrentContext; self.view.alpha = 0.0;
self.view.alpha = 0; [viewController presentViewController:navController
animated:NO
[viewController completion:^{
presentViewController:self
animated:NO // 1. Fade in the entire view.
completion:^{ [UIView animateWithDuration:0.1
UIWindow *window = [UIApplication sharedApplication].keyWindow; animations:^{
// During the presentation animation, we want to seamlessly animate the image self.view.alpha = 1.0;
// from its location in the conversation view. To do so, we need a }];
// consistent coordinate system, so we pass the `originRect` in the
// coordinate system of the window. // Make sure imageView is layed out before we update it's frame in the next
self.imageView.frame = [self.view convertRect:self.originRect // animation.
fromView:window]; [self.imageView.superview layoutIfNeeded];
[UIView animateWithDuration:0.25f // 2. Animate imageView from it's initial position, which should match where it was
delay:0 // in the presenting view to it's final position, front and center in this view. This
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut // animation intentionally overlaps the previous
animations:^() { [UIView animateWithDuration:0.2
self.view.alpha = 1.0f; delay:0.08
// During the presentation animation, we want to seamlessly animate the image options:UIViewAnimationOptionCurveEaseOut
// to its resting location in this view. We use `resizedFrameForImageView` animations:^(void) {
// to determine its size "at rest" in the content view, and then convert [self applyFinalImageViewConstraints];
// from the content view's coordinate system to the root view coordinate [self.imageView.superview layoutIfNeeded];
// system because the image view is temporarily hosted by the root view during // We must lay out *before* we centerImageViewConstraints
// the presentation animation. // because it uses the imageView.frame to build the contstraints
self.imageView.frame = [self resizedFrameForImageView:self.image.size]; // that will center the imageView, and then once again
self.imageView.center = [self.contentView convertPoint:self.contentView.center // to ensure that the centered constraints are applied.
fromView:self.contentView]; [self centerImageViewConstraints];
} [self.imageView.superview layoutIfNeeded];
completion:^(BOOL completed) { self.view.backgroundColor = UIColor.whiteColor;
self.scrollView.frame = self.contentView.bounds; }
[self.scrollView addSubview:self.imageView]; completion:^(BOOL finished) {
[self updateLayouts]; self.view.userInteractionEnabled = YES;
self.view.userInteractionEnabled = YES;
_isPresenting = NO; if (self.isVideo) {
}]; [self playVideo];
[UIUtil modalCompletionBlock](); }
}]; }];
} }];
}
- (void)dismiss {
- (void)dismiss
{
self.view.userInteractionEnabled = NO; self.view.userInteractionEnabled = NO;
[UIView animateWithDuration:0.25f [UIApplication sharedApplication].statusBarHidden = NO;
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear OWSAssert(self.imageView.superview);
animations:^() {
self.backgroundView.backgroundColor = [UIColor clearColor]; [self.imageView.superview layoutIfNeeded];
self.scrollView.alpha = 0;
self.view.alpha = 0; // Move the image view pack to it's initial position, i.e. where
// it sits on the screen in the conversation view.
[self applyInitialImageViewConstraints];
[UIView animateWithDuration:0.2
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^(void) {
[self.imageView.superview layoutIfNeeded];
// In case user has hidden bars, which changes background to black.
self.view.backgroundColor = UIColor.whiteColor;
// fade out content and toolbars
self.navigationController.view.alpha = 0.0;
} }
completion:^(BOOL completed) { completion:^(BOOL finished) {
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]; [self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
}]; }];
} }
#pragma mark - Update Layout #pragma mark - UIScrollViewDelegate
- (void)viewDidLayoutSubviews { - (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
[self updateLayouts]; {
return self.imageView;
} }
- (void)updateLayouts { - (void)centerImageViewConstraints
if (_isPresenting) { {
return; OWSAssert(self.scrollView);
}
self.scrollView.frame = self.contentView.bounds;
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
self.scrollView.contentSize = self.imageView.frame.size;
self.scrollView.contentInset = [self contentInsetForScrollView:self.scrollView.zoomScale];
}
#pragma mark - Resizing CGSize scrollViewSize = self.scrollView.bounds.size;
CGSize imageViewSize = self.imageView.frame.size;
- (CGRect)resizedFrameForImageView:(CGSize)imageSize { CGFloat yOffset = MAX(0, (scrollViewSize.height - imageViewSize.height) / 2);
CGRect frame = self.contentView.bounds; self.imageViewTopConstraint.constant = yOffset;
CGSize screenSize = self.imageViewBottomConstraint.constant = yOffset;
CGSizeMake(frame.size.width * self.scrollView.zoomScale, frame.size.height * self.scrollView.zoomScale);
CGSize targetSize = screenSize;
if ([self isImagePortrait]) { CGFloat xOffset = MAX(0, (scrollViewSize.width - imageViewSize.width) / 2);
if ([self getAspectRatioForCGSize:screenSize] < [self getAspectRatioForCGSize:imageSize]) { self.imageViewLeadingConstraint.constant = xOffset;
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize]; self.imageViewTrailingConstraint.constant = xOffset;
} else { }
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
}
} else {
if ([self getAspectRatioForCGSize:screenSize] > [self getAspectRatioForCGSize:imageSize]) {
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
} else {
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize];
}
}
frame.size = targetSize; - (void)scrollViewDidZoom:(UIScrollView *)scrollView
frame.origin = CGPointMake(0, 0); {
return frame; [self centerImageViewConstraints];
[self.view layoutIfNeeded];
} }
- (UIEdgeInsets)contentInsetForScrollView:(CGFloat)targetZoomScale { #pragma mark - Video Playback
UIEdgeInsets inset = UIEdgeInsetsZero;
CGSize boundsSize = self.scrollView.bounds.size; - (void)playVideo
CGSize contentSize = self.image.size; {
CGSize minSize; OWSAssert(self.isVideo);
OWSAssert(self.videoPlayer);
if ([self isImagePortrait]) { AVPlayerViewController *vc = [AVPlayerViewController new];
if ([self getAspectRatioForCGSize:boundsSize] < [self getAspectRatioForCGSize:contentSize]) { AVPlayer *player = self.videoPlayer;
minSize.height = boundsSize.height; vc.player = player;
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
} else {
minSize.width = boundsSize.width;
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
}
} else {
if ([self getAspectRatioForCGSize:boundsSize] > [self getAspectRatioForCGSize:contentSize]) {
minSize.width = boundsSize.width;
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
} else {
minSize.height = boundsSize.height;
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
}
}
CGSize finalSize = self.view.bounds.size; vc.modalPresentationStyle = UIModalPresentationCustom;
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
minSize.width *= targetZoomScale; // Rewind for repeated plays
minSize.height *= targetZoomScale; [player seekToTime:kCMTimeZero];
[self presentViewController:vc
animated:NO
completion:^(void) {
[player play];
}];
}
if (minSize.height > finalSize.height && minSize.width > finalSize.width) { - (void)playerItemDidPlayToCompletion:(NSNotification *)notification
inset = UIEdgeInsetsZero; {
} else { OWSAssert(self.isVideo);
CGFloat dy = boundsSize.height - minSize.height; OWSAssert(self.videoPlayer);
CGFloat dx = boundsSize.width - minSize.width; DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
dy = (dy > 0) ? dy : 0; [self dismissViewControllerAnimated:NO completion:nil];
dx = (dx > 0) ? dx : 0; }
inset.top = dy / 2.0f; - (void)moviePlayerPlaybackStateDidChange:(NSNotification *)notification
inset.bottom = dy / 2.0f; {
inset.left = dx / 2.0f; DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
inset.right = dx / 2.0f; OWSAssert(self.mpVideoPlayer);
}
return inset;
} }
#pragma mark - UIScrollViewDelegate - (void)moviePlayerWillEnterFullscreen:(NSNotification *)notification
{
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
OWSAssert(self.videoPlayer);
self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone;
}
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView - (void)moviePlayerDidEnterFullscreen:(NSNotification *)notification
{ {
return self.imageView; DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
OWSAssert(self.videoPlayer);
self.mpVideoPlayer.controlStyle = MPMovieControlStyleFullscreen;
} }
- (void)scrollViewDidZoom:(UIScrollView *)scrollView { // There's more than one way to exit the fullscreen video playback.
scrollView.contentInset = [self contentInsetForScrollView:scrollView.zoomScale]; // There's a done button, a "toggle fullscreen" button and I think
// there's some gestures too. These fire slightly different notifications.
// We want to hide & clean up the video player immediately in all of
// these cases.
- (void)moviePlayerWillExitFullscreen:(NSNotification *)notification
{
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
if (self.scrollView.scrollEnabled == NO) { // If we didn't just complete playback, user chose to exit fullscreen.
self.scrollView.scrollEnabled = YES; // In that case, we dismiss the view controller since the user is probably done.
} // if (!self.didJustCompleteVideoPlayback) {
// [self dismiss];
// }
// self.didJustCompleteVideoPlayback = NO;
self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone;
// [self clearVideoPlayer];
} }
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale // See comment on moviePlayerWillExitFullscreen:
- (void)moviePlayerDidExitFullscreen:(NSNotification *)notification
{ {
self.scrollView.scrollEnabled = (scale > 1); DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
self.scrollView.contentInset = [self contentInsetForScrollView:scale]; self.mpVideoPlayer.controlStyle = MPMovieControlStyleEmbedded;
// [self clearVideoPlayer];
} }
#pragma mark - Utility - (void)moviePlayerDidFinishPlayback:(NSNotification *)notification
{
- (BOOL)isImagePortrait { OWSAssert(self.videoPlayer);
return ([self getAspectRatioForCGSize:self.image.size] > 1.0f);
NSNumber *reason = notification.userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey];
DDLogDebug(@"%@ movie player finished with reason %@", self.logTag, reason);
OWSAssert(reason);
switch (reason.integerValue) {
case MPMovieFinishReasonPlaybackEnded: {
DDLogDebug(@"%@ video played to completion.", self.logTag);
self.mpVideoPlayer.controlStyle = MPMovieControlStyleNone;
[self.mpVideoPlayer setFullscreen:NO animated:YES];
break;
}
case MPMovieFinishReasonPlaybackError: {
DDLogDebug(@"%@ error playing video.", self.logTag);
break;
}
case MPMovieFinishReasonUserExited: {
// FIXME: unable to fire this (only tried on iOS11.2 so far)
DDLogDebug(@"%@ user exited video playback", self.logTag);
[self dismiss];
break;
}
}
} }
- (CGFloat)getAspectRatioForCGSize:(CGSize)size { //- (void)clearVideoPlayer
return size.height / size.width; //{
} // [self.videoPlayer stop];
// [self.videoPlayer.view removeFromSuperview];
// self.videoPlayer = nil;
//}
//- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer
//{
// _mpVideoPlayer = mpVideoPlayer;
//
// [ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil];
//}
#pragma mark - Saving images to Camera Roll #pragma mark - Saving images to Camera Roll

Loading…
Cancel
Save