Add audio attachment player.

* Fix two bugs around play/pause button appearance.
* Fix bugs around stopping playback when leaving view/entering background.
* Fix bugs around cleaning up playback state  when leaving view/entering background.
* Fix audio playback vs. hardware mute button.
* Improve handling of invalid audio attachments.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent 61d72f8e95
commit 980d726a48

@ -15,6 +15,7 @@
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */; };
344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F2F661E57A932000D9322 /* UIViewController+OWS.m */; };
34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */; };
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; };
345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671001E89A5F1006EE662 /* ThreadUtil.m */; };
3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */; };
@ -355,6 +356,8 @@
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = "<group>"; };
344F2F651E57A932000D9322 /* UIViewController+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+OWS.h"; path = "util/UIViewController+OWS.h"; sourceTree = "<group>"; };
344F2F661E57A932000D9322 /* UIViewController+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+OWS.m"; path = "util/UIViewController+OWS.m"; sourceTree = "<group>"; };
34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = "<group>"; };
34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioAttachmentPlayer.m; sourceTree = "<group>"; };
34535D801E256BE9008A4747 /* UIView+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OWS.h"; sourceTree = "<group>"; };
34535D811E256BE9008A4747 /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = "<group>"; };
345670FF1E89A5F1006EE662 /* ThreadUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadUtil.h; sourceTree = "<group>"; };
@ -871,6 +874,8 @@
34B3F8581E8DF1700035BE1A /* NotificationSettingsViewController.h */,
34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */,
34B3F85A1E8DF1700035BE1A /* OversizeTextMessageViewController.swift */,
34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */,
34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */,
34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsTableViewController.h */,
34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsTableViewController.m */,
34B3F85D1E8DF1700035BE1A /* OWSLinkDeviceViewController.h */,
@ -1995,6 +2000,7 @@
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */,
B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */,
34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */,
34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */,
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */,
45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */,
34B3F87A1E8DF1700035BE1A /* DebugUITableViewController.m in Sources */,

@ -9,9 +9,11 @@
#import "TSAttachmentStream.h"
#import "TSMessagesManager.h"
#import "TSStorageManager+keyingMaterial.h"
#import "UIView+OWS.h"
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <SCWaveformView.h>
#define AUDIO_BAR_HEIGHT 36
NS_ASSUME_NONNULL_BEGIN
@ -105,12 +107,14 @@ NS_ASSUME_NONNULL_BEGIN
}
- (void)setAudioIconToPlay {
[_audioPlayPauseButton setBackgroundImage:[UIImage imageNamed:@"audio_play_button_blue"]
[_audioPlayPauseButton
setBackgroundImage:[UIImage imageNamed:(_incoming ? @"audio_play_button_blue" : @"audio_play_button")]
forState:UIControlStateNormal];
}
- (void)setAudioIconToPause {
[_audioPlayPauseButton setBackgroundImage:[UIImage imageNamed:@"audio_pause_button_blue"]
[_audioPlayPauseButton
setBackgroundImage:[UIImage imageNamed:(_incoming ? @"audio_pause_button_blue" : @"audio_pause_button")]
forState:UIControlStateNormal];
}
@ -167,8 +171,6 @@ NS_ASSUME_NONNULL_BEGIN
audioBubble.layer.masksToBounds = YES;
_audioPlayPauseButton = [[UIButton alloc] initWithFrame:CGRectMake(3, 3, 30, 30)];
[_audioPlayPauseButton setBackgroundImage:[UIImage imageNamed:@"audio_play_button"]
forState:UIControlStateNormal];
_audioPlayPauseButton.enabled = NO;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&err];
@ -189,8 +191,6 @@ NS_ASSUME_NONNULL_BEGIN
_waveform.normalColor = [UIColor whiteColor];
_waveform.progressColor =
[UIColor colorWithRed:107 / 255.0f green:185 / 255.0f blue:254 / 255.0f alpha:1.0f];
[_audioPlayPauseButton setBackgroundImage:[UIImage imageNamed:@"audio_play_button_blue"]
forState:UIControlStateNormal];
_durationLabel.textColor = [UIColor darkTextColor];
}
@ -204,6 +204,12 @@ NS_ASSUME_NONNULL_BEGIN
attachmentStateCallback:nil];
}
if (self.isAudioPlaying) {
[self setAudioIconToPause];
} else {
[self setAudioIconToPlay];
}
return audioBubble;
} else {
// Unknown media type.

@ -37,9 +37,6 @@ extern NSString *const OWSMessagesViewControllerDidAppearNotification;
@property (nonatomic, readonly) TSThread *thread;
@property (nonatomic, strong) MPMoviePlayerController *videoPlayer;
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, strong) AVAudioRecorder *audioRecorder;
- (void)configureForThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardAppearing

@ -13,6 +13,7 @@
#import "FullImageViewController.h"
#import "NSDate+millisecondTimeStamp.h"
#import "NewGroupViewController.h"
#import "OWSAudioAttachmentPlayer.h"
#import "OWSCall.h"
#import "OWSCallCollectionViewCell.h"
#import "OWSContactsManager.h"
@ -201,8 +202,9 @@ typedef enum : NSUInteger {
@property (nonatomic) JSQMessagesBubbleImage *currentlyOutgoingBubbleImageData;
@property (nonatomic) JSQMessagesBubbleImage *outgoingMessageFailedImageData;
@property (nonatomic) NSTimer *audioPlayerPoller;
@property (nonatomic) TSVideoAttachmentAdapter *currentMediaAdapter;
@property (nonatomic) MPMoviePlayerController *videoPlayer;
@property (nonatomic) AVAudioRecorder *audioRecorder;
@property (nonatomic) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
@property (nonatomic) NSTimer *readTimer;
@property (nonatomic) UIView *navigationBarTitleView;
@ -745,8 +747,7 @@ typedef enum : NSUInteger {
// Since we're using a custom back button, we have to do some extra work to manage the interactivePopGestureRecognizer
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
[_audioPlayerPoller invalidate];
[_audioPlayer stop];
[self.audioAttachmentPlayer stop];
// reset all audio bars to 0
JSQMessagesCollectionView *collectionView = self.collectionView;
@ -1752,7 +1753,6 @@ typedef enum : NSUInteger {
// fileurl disappeared should look up in db as before. will do refactor
// full screen, check this setup with a .mov
TSVideoAttachmentAdapter *messageMedia = (TSVideoAttachmentAdapter *)[messageItem media];
_currentMediaAdapter = messageMedia;
__block TSAttachment *attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment =
@ -1788,81 +1788,19 @@ typedef enum : NSUInteger {
[_videoPlayer setFullscreen:YES animated:NO];
}
} else if ([messageMedia isAudio]) {
if (messageMedia.isAudioPlaying) {
// if you had started playing an audio msg and now you're tapping it to pause
messageMedia.isAudioPlaying = NO;
[_audioPlayer pause];
messageMedia.isPaused = YES;
[_audioPlayerPoller invalidate];
double current = [_audioPlayer currentTime] / [_audioPlayer duration];
[messageMedia setAudioProgressFromFloat:(float)current];
[messageMedia setAudioIconToPlay];
} else {
BOOL isResuming = NO;
[_audioPlayerPoller invalidate];
// loop through all the other bubbles and set their isPlaying to false
NSInteger num_bubbles = [self collectionView:collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < num_bubbles; i++) {
NSIndexPath *indexPathI = [NSIndexPath indexPathForRow:i inSection:0];
id<OWSMessageData> message = [self messageAtIndexPath:indexPathI];
if (message.messageType == TSIncomingMessageAdapter && message.isMediaMessage &&
[[message media] isKindOfClass:[TSVideoAttachmentAdapter class]]) {
TSVideoAttachmentAdapter *msgMedia
= (TSVideoAttachmentAdapter *)[message media];
if ([msgMedia isAudio]) {
if (msgMedia == messageMedia && messageMedia.isPaused) {
isResuming = YES;
} else {
msgMedia.isAudioPlaying = NO;
msgMedia.isPaused = NO;
[msgMedia setAudioIconToPlay];
[msgMedia setAudioProgressFromFloat:0];
[msgMedia resetAudioDuration];
}
}
}
}
if (isResuming) {
// if you had paused an audio msg and now you're tapping to resume
[_audioPlayer prepareToPlay];
[_audioPlayer play];
[messageMedia setAudioIconToPause];
messageMedia.isAudioPlaying = YES;
messageMedia.isPaused = NO;
_audioPlayerPoller =
[NSTimer scheduledTimerWithTimeInterval:.05
target:self
selector:@selector(audioPlayerUpdated:)
userInfo:@{
@"adapter" : messageMedia
}
repeats:YES];
} else {
// if you are tapping an audio msg for the first time to play
messageMedia.isAudioPlaying = YES;
NSError *error;
_audioPlayer =
[[AVAudioPlayer alloc] initWithContentsOfURL:attStream.mediaURL error:&error];
if (error) {
DDLogError(@"error: %@", error);
}
[_audioPlayer prepareToPlay];
[_audioPlayer play];
[messageMedia setAudioIconToPause];
_audioPlayer.delegate = self;
_audioPlayerPoller =
[NSTimer scheduledTimerWithTimeInterval:.05
target:self
selector:@selector(audioPlayerUpdated:)
userInfo:@{
@"adapter" : messageMedia
}
repeats:YES];
if (self.audioAttachmentPlayer) {
if (self.audioAttachmentPlayer.mediaAdapter == messageMedia) {
// Tap to pause & unpause.
[self.audioAttachmentPlayer togglePlayState];
return;
}
[self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil;
}
self.audioAttachmentPlayer =
[[OWSAudioAttachmentPlayer alloc] initWithMediaAdapter:messageMedia
databaseConnection:self.uiDatabaseConnection];
[self.audioAttachmentPlayer play];
}
}
} else if ([messageItem.media isKindOfClass:[AttachmentPointerAdapter class]]) {
@ -2685,20 +2623,6 @@ typedef enum : NSUInteger {
[_audioRecorder prepareToRecord];
}
- (void)audioPlayerUpdated:(NSTimer *)timer {
double current = [_audioPlayer currentTime] / [_audioPlayer duration];
double interval = [_audioPlayer duration] - [_audioPlayer currentTime];
[_currentMediaAdapter setDurationOfAudio:interval];
[_currentMediaAdapter setAudioProgressFromFloat:(float)current];
}
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
[_audioPlayerPoller invalidate];
[_currentMediaAdapter setAudioProgressFromFloat:0];
[_currentMediaAdapter setDurationOfAudio:_audioPlayer.duration];
[_currentMediaAdapter setAudioIconToPlay];
}
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
if (flag) {
NSData *audioData = [NSData dataWithContentsOfURL:recorder.url];

@ -0,0 +1,22 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <AVFoundation/AVFoundation.h>
@class TSVideoAttachmentAdapter;
@class YapDatabaseConnection;
@interface OWSAudioAttachmentPlayer : NSObject <AVAudioPlayerDelegate>
@property (nonatomic, readonly) TSVideoAttachmentAdapter *mediaAdapter;
- (instancetype)initWithMediaAdapter:(TSVideoAttachmentAdapter *)mediaAdapter
databaseConnection:(YapDatabaseConnection *)databaseConnection;
- (void)play;
- (void)pause;
- (void)stop;
- (void)togglePlayState;
@end

@ -0,0 +1,173 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSAudioAttachmentPlayer.h"
#import "TSAttachment.h"
#import "TSAttachmentStream.h"
#import "TSVideoAttachmentAdapter.h"
#import "ViewControllerUtils.h"
#import <YapDatabase/YapDatabaseConnection.h>
@interface OWSAudioAttachmentPlayer ()
@property (nonatomic) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) AVAudioPlayer *audioPlayer;
@property (nonatomic, nullable) NSTimer *audioPlayerPoller;
@end
#pragma mark -
@implementation OWSAudioAttachmentPlayer
- (instancetype)initWithMediaAdapter:(TSVideoAttachmentAdapter *)mediaAdapter
databaseConnection:(YapDatabaseConnection *)databaseConnection
{
self = [super init];
if (!self) {
return self;
}
OWSAssert(mediaAdapter);
OWSAssert([mediaAdapter isAudio]);
OWSAssert(mediaAdapter.attachmentId);
OWSAssert(databaseConnection);
_mediaAdapter = mediaAdapter;
__block TSAttachment *attachment = nil;
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:mediaAdapter.attachmentId transaction:transaction];
}];
OWSAssert(attachment);
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
self.attachmentStream = (TSAttachmentStream *)attachment;
}
OWSAssert(self.attachmentStream);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self stop];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
[self stop];
}
#pragma mark - Methods
- (void)play
{
OWSAssert(self.attachmentStream);
OWSAssert(![self.mediaAdapter isAudioPlaying]);
[ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:YES];
[self.audioPlayerPoller invalidate];
self.mediaAdapter.isAudioPlaying = YES;
self.mediaAdapter.isPaused = NO;
[self.mediaAdapter setAudioIconToPause];
if (!self.audioPlayer) {
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.attachmentStream.mediaURL error:&error];
if (error) {
DDLogError(@"%@ error: %@", self.tag, error);
[self stop];
return;
}
self.audioPlayer.delegate = self;
}
[self.audioPlayer prepareToPlay];
[self.audioPlayer play];
self.audioPlayerPoller = [NSTimer scheduledTimerWithTimeInterval:.05
target:self
selector:@selector(audioPlayerUpdated:)
userInfo:nil
repeats:YES];
}
- (void)pause
{
OWSAssert(self.attachmentStream);
self.mediaAdapter.isAudioPlaying = NO;
self.mediaAdapter.isPaused = YES;
[self.audioPlayer pause];
[self.audioPlayerPoller invalidate];
double current = [self.audioPlayer currentTime] / [_audioPlayer duration];
[self.mediaAdapter setAudioProgressFromFloat:(float)current];
[self.mediaAdapter setAudioIconToPlay];
}
- (void)stop
{
OWSAssert(self.attachmentStream);
[self.audioPlayer pause];
[self.audioPlayerPoller invalidate];
[self.mediaAdapter setAudioProgressFromFloat:0];
[self.mediaAdapter setDurationOfAudio:_audioPlayer.duration];
[self.mediaAdapter setAudioIconToPlay];
self.mediaAdapter.isAudioPlaying = NO;
self.mediaAdapter.isPaused = NO;
}
- (void)togglePlayState
{
OWSAssert(self.attachmentStream);
if (self.mediaAdapter.isAudioPlaying) {
[self pause];
} else {
[self play];
}
}
#pragma mark - Events
- (void)audioPlayerUpdated:(NSTimer *)timer
{
OWSAssert(self.audioPlayer);
OWSAssert(self.audioPlayerPoller);
double current = [self.audioPlayer currentTime] / [self.audioPlayer duration];
double interval = [self.audioPlayer duration] - [self.audioPlayer currentTime];
[self.mediaAdapter setDurationOfAudio:interval];
[self.mediaAdapter setAudioProgressFromFloat:(float)current];
}
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
[self stop];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
Loading…
Cancel
Save