Skip to content

Commit 7c9849b

Browse files
Lucashuang0802jessesquires
authored andcommitted
Finished video thumbnail feature (jessesquires#1823)
- Close jessesquires#628 - Close jessesquires#709 - Close jessesquires#1408 - Close jessesquires#1496
1 parent cbde6c1 commit 7c9849b

File tree

11 files changed

+184
-7
lines changed

11 files changed

+184
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ This release closes the [8.0.0 milestone](https://github.com/jessesquires/JSQMes
2323
- Animated typing indicator. Typing indicator now animates like iMessage. (#1382) Thanks @radekcieciwa!
2424
- Dynamic text support. (#497, #1747) Thanks @MacMeDan!
2525
- Message cells now have a customizable accessory view. (#1519, #1719) Thanks @adubr!
26-
- Send button now can be turned on/off manually. (#1575 #1609) Thanks @sebastianludwig!
26+
- Send button now can be turned on/off manually. (#1575, #1609) Thanks @sebastianludwig!
27+
- Video message items now have a custom thumbnail option. (#628, #709, #1408, #1823) Thanks @weekwood, @benjaminhallock!
28+
- A new class `JSQMessagesVideoThumbnailFactory` now can generate thumbnail images from `AVURLAsset`. (#709, #1823) Thanks @weekwood, @Lucashuang0802!
2729

2830
### Fixes
2931

JSQMessages.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
88C00A501A44D4D800B004B3 /* JSQPhotoMediaItemTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88C00A4F1A44D4D800B004B3 /* JSQPhotoMediaItemTests.m */; };
107107
88C00A521A44D4E500B004B3 /* JSQVideoMediaItemTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88C00A511A44D4E500B004B3 /* JSQVideoMediaItemTests.m */; };
108108
88C4583019F5F7A0008FD427 /* JSQMessagesMediaViewBubbleImageMasker.m in Sources */ = {isa = PBXBuildFile; fileRef = 88C4582F19F5F7A0008FD427 /* JSQMessagesMediaViewBubbleImageMasker.m */; };
109+
A04B0EBF1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = A04B0EBE1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.m */; };
109110
BF10D6AA1D062AD10072D215 /* JSQMessagesTypingView.m in Sources */ = {isa = PBXBuildFile; fileRef = BF10D6A91D062AD10072D215 /* JSQMessagesTypingView.m */; };
110111
/* End PBXBuildFile section */
111112

@@ -269,6 +270,8 @@
269270
88C00A511A44D4E500B004B3 /* JSQVideoMediaItemTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQVideoMediaItemTests.m; sourceTree = "<group>"; };
270271
88C4582E19F5F7A0008FD427 /* JSQMessagesMediaViewBubbleImageMasker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQMessagesMediaViewBubbleImageMasker.h; sourceTree = "<group>"; };
271272
88C4582F19F5F7A0008FD427 /* JSQMessagesMediaViewBubbleImageMasker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesMediaViewBubbleImageMasker.m; sourceTree = "<group>"; };
273+
A04B0EBD1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQMessagesVideoThumbnailFactory.h; sourceTree = "<group>"; };
274+
A04B0EBE1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesVideoThumbnailFactory.m; sourceTree = "<group>"; };
272275
BF10D6A81D062AD10072D215 /* JSQMessagesTypingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQMessagesTypingView.h; sourceTree = "<group>"; };
273276
BF10D6A91D062AD10072D215 /* JSQMessagesTypingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesTypingView.m; sourceTree = "<group>"; };
274277
/* End PBXFileReference section */
@@ -451,6 +454,8 @@
451454
88A25F6B19D8E01A00924534 /* JSQMessagesTimestampFormatter.m */,
452455
88A25F6C19D8E01A00924534 /* JSQMessagesToolbarButtonFactory.h */,
453456
88A25F6D19D8E01A00924534 /* JSQMessagesToolbarButtonFactory.m */,
457+
A04B0EBD1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.h */,
458+
A04B0EBE1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.m */,
454459
);
455460
path = Factories;
456461
sourceTree = "<group>";
@@ -741,6 +746,7 @@
741746
88A25FCB19D8E01A00924534 /* JSQMessagesCollectionViewCell.m in Sources */,
742747
88A25FBB19D8E01A00924534 /* JSQMessagesViewController.m in Sources */,
743748
8885734D19DE55D000E89D20 /* NSUserDefaults+DemoSettings.m in Sources */,
749+
A04B0EBF1D6ADE5800FBDC47 /* JSQMessagesVideoThumbnailFactory.m in Sources */,
744750
544A32211CB2EE380084BFC0 /* JSQAudioMediaViewAttributes.m in Sources */,
745751
883C11781A09FB100092A16D /* JSQMessagesCellTextView.m in Sources */,
746752
88A25FB919D8E01A00924534 /* UIView+JSQMessages.m in Sources */,

JSQMessagesDemo/Base.lproj/Localizable.strings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"Send photo" = "Send photo";
2222
"Send location" = "Send location";
2323
"Send video" = "Send video";
24+
"Send video thumbnail" = "Send video with thumbnail";
2425
"Custom Action" = "Custom Action";
2526
"OK" = "OK";
2627

JSQMessagesDemo/DemoMessagesViewController.m

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ - (void)didPressAccessoryButton:(UIButton *)sender
365365
delegate:self
366366
cancelButtonTitle:NSLocalizedString(@"Cancel", nil)
367367
destructiveButtonTitle:nil
368-
otherButtonTitles:NSLocalizedString(@"Send photo", nil), NSLocalizedString(@"Send location", nil), NSLocalizedString(@"Send video", nil), NSLocalizedString(@"Send audio", nil), nil];
368+
otherButtonTitles:NSLocalizedString(@"Send photo", nil), NSLocalizedString(@"Send location", nil), NSLocalizedString(@"Send video", nil), NSLocalizedString(@"Send video thumbnail", nil), NSLocalizedString(@"Send audio", nil), nil];
369369

370370
[sheet showFromToolbar:self.inputToolbar];
371371
}
@@ -397,6 +397,10 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn
397397
break;
398398

399399
case 3:
400+
[self.demoData addVideoMediaMessageWithThumbnail];
401+
break;
402+
403+
case 4:
400404
[self.demoData addAudioMediaMessage];
401405
break;
402406
}

JSQMessagesDemo/DemoModelData.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ static NSString * const kJSQDemoAvatarIdWoz = @"309-41802-93823";
5858

5959
- (void)addVideoMediaMessage;
6060

61+
- (void)addVideoMediaMessageWithThumbnail;
62+
6163
- (void)addAudioMediaMessage;
6264

6365
@end

JSQMessagesDemo/DemoModelData.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,16 @@ - (void)addVideoMediaMessage
200200
[self.messages addObject:videoMessage];
201201
}
202202

203+
- (void)addVideoMediaMessageWithThumbnail
204+
{
205+
// don't have a real video, just pretending
206+
NSURL *videoURL = [NSURL URLWithString:@"file://"];
207+
208+
JSQVideoMediaItem *videoItem = [[JSQVideoMediaItem alloc] initWithFileURL:videoURL isReadyToPlay:YES thumbnailImage:[UIImage imageNamed:@"goldengate"]];
209+
JSQMessage *videoMessage = [JSQMessage messageWithSenderId:kJSQDemoAvatarIdSquires
210+
displayName:kJSQDemoAvatarDisplayNameSquires
211+
media:videoItem];
212+
[self.messages addObject:videoMessage];
213+
}
214+
203215
@end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// JSQMessagesVideoThumbnailFactory.h
3+
// JSQMessages
4+
//
5+
// Created by Xi Huang on 8/22/16.
6+
// Copyright © 2016 Hexed Bits. All rights reserved.
7+
//
8+
9+
#import <AVFoundation/AVFoundation.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
/**
14+
* A completion handler block for a `JSQMessagesVideoThumbnailFactory`. See `customThumbnailWithVideoMediaAsset: withCompletionHandler:`.
15+
*/
16+
typedef void (^JSQMessagesVideoThumbnailCompletionBlock)(UIImage * _Nullable, NSError * _Nullable);
17+
18+
/**
19+
* `JSQMessagesVideoThumbnailFactory` is a factory that provides a means for generating a thumbnail image with a given media item.
20+
*/
21+
@interface JSQMessagesVideoThumbnailFactory : NSObject
22+
23+
/**
24+
* Generates and returns a thumbnail image for the specified video media asset with the default CMTimeMakeWithSeconds(1, 2).
25+
*
26+
* The specified block is executed upon completion of generating the thumbnail image and is executed on the app’s main thread.
27+
*
28+
* @param videoMediaAsset The instance of AVURLAsset for the video media item.
29+
* @param completion The block to call after the thumbnail has been generated.
30+
*/
31+
+ (void)customThumbnailWithVideoMediaAsset:(AVURLAsset *)videoMediaAsset
32+
withCompletionHandler:(JSQMessagesVideoThumbnailCompletionBlock)completion;
33+
34+
/**
35+
* Generates and returns a thumbnail image for the specified video media asset with a given CMTime.
36+
*
37+
* The specified block is executed upon completion of generating the thumbnail image and is executed on the app’s main thread.
38+
*
39+
* @param videoMediaAsset The instance of AVURLAsset for the video media item.
40+
* @param time The CMTime struct for capturing the thumbnail image from the video media item.
41+
* @param completion The block to call after the thumbnail has been generated.
42+
*/
43+
+ (void)customThumbnailWithVideoMediaAsset:(AVURLAsset *)videoMediaAsset
44+
time:(CMTime)time
45+
withCompletionHandler:(JSQMessagesVideoThumbnailCompletionBlock)completion;
46+
47+
@end
48+
49+
NS_ASSUME_NONNULL_END
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// JSQMessagesVideoThumbnailFactory.m
3+
// JSQMessages
4+
//
5+
// Created by Xi Huang on 8/22/16.
6+
// Copyright © 2016 Hexed Bits. All rights reserved.
7+
//
8+
9+
#import "JSQVideoMediaItem.h"
10+
11+
#import "JSQMessagesVideoThumbnailFactory.h"
12+
#import "JSQMessagesBubbleImageFactory.h"
13+
14+
#import "UIImage+JSQMessages.h"
15+
16+
@implementation JSQMessagesVideoThumbnailFactory
17+
18+
+ (void)customThumbnailWithVideoMediaAsset:(AVURLAsset *)videoMediaAsset
19+
withCompletionHandler:(JSQMessagesVideoThumbnailCompletionBlock)completion {
20+
21+
[JSQMessagesVideoThumbnailFactory customThumbnailWithVideoMediaAsset:videoMediaAsset
22+
time:CMTimeMakeWithSeconds(1, 2)
23+
withCompletionHandler:completion];
24+
}
25+
26+
+ (void)customThumbnailWithVideoMediaAsset:(AVURLAsset *)videoMediaAsset
27+
time:(CMTime)time
28+
withCompletionHandler:(JSQMessagesVideoThumbnailCompletionBlock)completion {
29+
30+
NSParameterAssert(videoMediaAsset != nil);
31+
32+
AVAssetImageGenerator *generate = [[AVAssetImageGenerator alloc] initWithAsset:videoMediaAsset];
33+
generate.appliesPreferredTrackTransform = YES;
34+
35+
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
36+
generate.maximumSize = CGSizeMake(315.0f, 225.0f);
37+
}
38+
else {
39+
generate.maximumSize = CGSizeMake(210.0f, 150.0f);
40+
}
41+
[generate generateCGImagesAsynchronouslyForTimes:[NSArray arrayWithObject:[NSValue valueWithCMTime:time]]
42+
completionHandler:^(CMTime requestedTime, CGImageRef im, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error){
43+
44+
if (completion) {
45+
if (result == AVAssetImageGeneratorSucceeded) {
46+
completion([UIImage imageWithCGImage:im], nil);
47+
}
48+
else {
49+
completion(nil, error);
50+
}
51+
}
52+
}];
53+
}
54+
55+
@end

JSQMessagesViewController/Model/JSQVideoMediaItem.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ NS_ASSUME_NONNULL_BEGIN
3333
*/
3434
@property (nonatomic, strong, nullable) NSURL *fileURL;
3535

36+
/**
37+
* Adding an image to mask behind the video.
38+
*/
39+
@property (nonatomic, strong, nullable) UIImage *thumbnailImage;
40+
3641
/**
3742
* A boolean value that specifies whether or not the video is ready to be played.
3843
*
@@ -55,6 +60,23 @@ NS_ASSUME_NONNULL_BEGIN
5560
*/
5661
- (instancetype)initWithFileURL:(nullable NSURL *)fileURL isReadyToPlay:(BOOL)isReadyToPlay;
5762

63+
/**
64+
* Initializes and returns a video media item having the given fileURL.
65+
*
66+
* @param fileURL The URL that identifies the video resource.
67+
* @param isReadyToPlay A boolean value that specifies if the video is ready to play.
68+
* @param thumbnailImage The background thumbnail for the video.
69+
*
70+
* @return An initialized `JSQVideoMediaItem` if successful, `nil` otherwise.
71+
*
72+
* @discussion If the video must be downloaded from the network,
73+
* you may initialize a `JSQVideoMediaItem` with a `nil` fileURL or specify `NO` for
74+
* isReadyToPlay. Once the video has been saved to disk, or is ready to stream, you can
75+
* set the fileURL property or isReadyToPlay property, respectively. The background thumbnail
76+
* is optional.
77+
*/
78+
- (instancetype)initWithFileURL:(NSURL *)fileURL isReadyToPlay:(BOOL)isReadyToPlay thumbnailImage:(nullable UIImage *)thumbnailImage;
79+
5880
@end
5981

6082
NS_ASSUME_NONNULL_END

JSQMessagesViewController/Model/JSQVideoMediaItem.m

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#import "JSQMessagesMediaPlaceholderView.h"
2222
#import "JSQMessagesMediaViewBubbleImageMasker.h"
23+
#import "JSQMessagesVideoThumbnailFactory.h"
2324

2425
#import "UIImage+JSQMessages.h"
2526

@@ -36,12 +37,18 @@ @implementation JSQVideoMediaItem
3637
#pragma mark - Initialization
3738

3839
- (instancetype)initWithFileURL:(NSURL *)fileURL isReadyToPlay:(BOOL)isReadyToPlay
40+
{
41+
return [self initWithFileURL:fileURL isReadyToPlay:isReadyToPlay thumbnailImage:nil];
42+
}
43+
44+
- (instancetype)initWithFileURL:(NSURL *)fileURL isReadyToPlay:(BOOL)isReadyToPlay thumbnailImage:(UIImage *)thumbnailImage
3945
{
4046
self = [super init];
4147
if (self) {
4248
_fileURL = [fileURL copy];
4349
_isReadyToPlay = isReadyToPlay;
4450
_cachedVideoImageView = nil;
51+
_thumbnailImage = thumbnailImage;
4552
}
4653
return self;
4754
}
@@ -85,12 +92,25 @@ - (UIView *)mediaView
8592
UIImage *playIcon = [[UIImage jsq_defaultPlayImage] jsq_imageMaskedWithColor:[UIColor lightGrayColor]];
8693

8794
UIImageView *imageView = [[UIImageView alloc] initWithImage:playIcon];
88-
imageView.backgroundColor = [UIColor blackColor];
8995
imageView.frame = CGRectMake(0.0f, 0.0f, size.width, size.height);
9096
imageView.contentMode = UIViewContentModeCenter;
9197
imageView.clipsToBounds = YES;
9298
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:imageView isOutgoing:self.appliesMediaViewMaskAsOutgoing];
93-
self.cachedVideoImageView = imageView;
99+
100+
if (_thumbnailImage) {
101+
UIImageView *thumbnailImageView = [[UIImageView alloc] initWithImage:_thumbnailImage];
102+
thumbnailImageView.frame = CGRectMake(0.0f, 0.0f, size.width, size.height);
103+
thumbnailImageView.contentMode = UIViewContentModeCenter;
104+
thumbnailImageView.clipsToBounds = YES;
105+
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:thumbnailImageView isOutgoing:self.appliesMediaViewMaskAsOutgoing];
106+
imageView.backgroundColor = [UIColor clearColor];
107+
[thumbnailImageView addSubview:imageView];
108+
self.cachedVideoImageView = thumbnailImageView;
109+
}
110+
else {
111+
imageView.backgroundColor = [UIColor blackColor];
112+
self.cachedVideoImageView = imageView;
113+
}
94114
}
95115

96116
return self.cachedVideoImageView;
@@ -122,8 +142,8 @@ - (NSUInteger)hash
122142

123143
- (NSString *)description
124144
{
125-
return [NSString stringWithFormat:@"<%@: fileURL=%@, isReadyToPlay=%@, appliesMediaViewMaskAsOutgoing=%@>",
126-
[self class], self.fileURL, @(self.isReadyToPlay), @(self.appliesMediaViewMaskAsOutgoing)];
145+
return [NSString stringWithFormat:@"<%@: fileURL=%@, isReadyToPlay=%@, appliesMediaViewMaskAsOutgoing=%@>, thumbnailImage=%@",
146+
[self class], self.fileURL, @(self.isReadyToPlay), @(self.appliesMediaViewMaskAsOutgoing), self.thumbnailImage];
127147
}
128148

129149
#pragma mark - NSCoding
@@ -134,6 +154,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder
134154
if (self) {
135155
_fileURL = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(fileURL))];
136156
_isReadyToPlay = [aDecoder decodeBoolForKey:NSStringFromSelector(@selector(isReadyToPlay))];
157+
_thumbnailImage = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(thumbnailImage))];
137158
}
138159
return self;
139160
}
@@ -143,14 +164,16 @@ - (void)encodeWithCoder:(NSCoder *)aCoder
143164
[super encodeWithCoder:aCoder];
144165
[aCoder encodeObject:self.fileURL forKey:NSStringFromSelector(@selector(fileURL))];
145166
[aCoder encodeBool:self.isReadyToPlay forKey:NSStringFromSelector(@selector(isReadyToPlay))];
167+
[aCoder encodeBool:self.thumbnailImage forKey:NSStringFromSelector(@selector(thumbnailImage))];
146168
}
147169

148170
#pragma mark - NSCopying
149171

150172
- (instancetype)copyWithZone:(NSZone *)zone
151173
{
152174
JSQVideoMediaItem *copy = [[[self class] allocWithZone:zone] initWithFileURL:self.fileURL
153-
isReadyToPlay:self.isReadyToPlay];
175+
isReadyToPlay:self.isReadyToPlay
176+
thumbnailImage:self.thumbnailImage];
154177
copy.appliesMediaViewMaskAsOutgoing = self.appliesMediaViewMaskAsOutgoing;
155178
return copy;
156179
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Read the docs, [available here][docsLink] via [@CocoaDocs](https://twitter.com/C
7474
- Harlan Haskans ([**@harlanhaskins**](https://github.com/harlanhaskins))
7575
- Eli Burke ([**@eliburke**](https://github.com/eliburke))
7676
- Sebastian Ludwig ([**@sebastianludwig**](https://github.com/sebastianludwig))
77+
- Lucas Huang ([**@Lucashuang0802**](https://github.com/Lucashuang0802))
7778

7879
## Contributing
7980

0 commit comments

Comments
 (0)