Skip to content
This repository has been archived by the owner on Dec 18, 2022. It is now read-only.

Commit

Permalink
0.6.1: Upload snaps with overlays
Browse files Browse the repository at this point in the history
- Fixed overlays being ignored and not uploaded
- Fixed thumbnails not being uploaded for video stories
  • Loading branch information
NSExceptional committed Feb 23, 2016
1 parent ff1f1da commit 2709bdc
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 52 deletions.
10 changes: 8 additions & 2 deletions Example/SnapchatKit-OSX/SnapchatKit-OSX/main.m
Expand Up @@ -223,8 +223,10 @@ int main(int argc, const char * argv[]) {
[SKClient sharedClient].casperUserAgent = kCasperUserAgent;
#endif

// [[SKClient sharedClient] signInWithUsername:kUsername password:kPassword completion:^(NSDictionary *dict, NSError *error) {
[[SKClient sharedClient] restoreSessionWithUsername:kUsername snapchatAuthToken:kAuthToken doGetUpdates:^(NSError *error) {
SKBlob *blob = [SKBlob blobWithContentsOfPath:@"/Users/tantan/Desktop/upload"];

[[SKClient sharedClient] signInWithUsername:kUsername password:kPassword completion:^(NSDictionary *dict, NSError *error) {
// [[SKClient sharedClient] restoreSessionWithUsername:kUsername snapchatAuthToken:kAuthToken doGetUpdates:^(NSError *error) {
if (!error) {
SKSession *session = [SKClient sharedClient].currentSession;
[[session valueForKey:@"_JSON"] writeToFile:[directory stringByAppendingPathComponent:@"current-session.plist"] atomically:YES];
Expand Down Expand Up @@ -260,6 +262,10 @@ int main(int argc, const char * argv[]) {
// }
// }];

[[SKClient sharedClient] postStory:blob for:0 completion:^(NSError *error) {
NSLog(@"%@", error);
}];


// Get unread snaps
NSArray *unread = session.unread;
Expand Down
9 changes: 8 additions & 1 deletion Pod/Classes/Model/SKBlob.h
Expand Up @@ -11,6 +11,8 @@

@class SKStory;

extern NSData * SKThumbnailFromGCImage(CGImageRef image);

/** A wrapper for the various kinds of data used throughout the API. */
@interface SKBlob : NSObject

Expand Down Expand Up @@ -46,10 +48,15 @@
@property (nonatomic, readonly) NSData *data;
/** The overlay for the video. \c nil if not applicable. */
@property (nonatomic, readonly) NSData *overlay;
/** Lazily initialized. The compressed data for the snap should it be uploaded. nil if not need be compressed. */
@property (nonatomic, readonly) NSData *zipData;
/** Lazily initialized.
@discussion The thumbnail for the video to be uploaded. nil if not applicable.
@note You may assign your own if you wish, and it will be used instead of the default one. */
@property (nonatomic ) NSData *videoThumbnail;
/** \c YES if the data is for a JPEG, \c NO if it's something other than a JPEG or PNG. */
@property (nonatomic, readonly) BOOL isImage;
/** \c YES if the data is for a MPEG4 video, \c NO if it's something else. */
@property (nonatomic, readonly) BOOL isVideo;


@end
127 changes: 120 additions & 7 deletions Pod/Classes/Model/SKBlob.m
Expand Up @@ -9,13 +9,15 @@
#import "SKBlob.h"
#import "SnapchatKit-Constants.h"
#import "NSData+SnapchatKit.h"
#import "NSString+SnapchatKit.h"
#import "SKStory.h"

#import "SKRequest.h"

#import "SSZipArchive.h"
@import AVFoundation;

@implementation SKBlob
@synthesize zipData = _zipData;

+ (instancetype)blobWithContentsOfPath:(NSString *)path {
return [[self alloc] initWithContentsOfPath:path];
Expand Down Expand Up @@ -129,10 +131,10 @@ - (id)initWithContentsOfPath:(NSString *)path {
}

// Delete file(s)
// NSError *error = nil;
// [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
// if (error && kVerboseLog)
// SKLog(@"Error deleting blob: %@", error);
// NSError *error = nil;
// [[NSFileManager defaultManager] removeItemAtPath:path error:&error];
// if (error && kVerboseLog)
// SKLog(@"Error deleting blob: %@", error);
} else {
return nil;
}
Expand Down Expand Up @@ -169,8 +171,8 @@ - (NSArray *)writeToPath:(NSString *)path filename:(NSString *)filename atomical
return @[path];
} else {
path = [path stringByAppendingPathComponent:filename];
NSString *overlay = [path stringByAppendingPathComponent:[filename stringByAppendingString:[@"-overlay" stringByAppendingString:self.overlay.appropriateFileExtension]]];
NSString *video = [path stringByAppendingPathComponent:[filename stringByAppendingString:self.data.appropriateFileExtension]];
NSString *overlay = [path stringByAppendingPathComponent:[filename stringByAppendingString:[@"-overlay" stringByAppendingString:_overlay.appropriateFileExtension]]];
NSString *video = [path stringByAppendingPathComponent:[filename stringByAppendingString:_data.appropriateFileExtension]];
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
[self.data writeToFile:video atomically:atomically];
[self.overlay writeToFile:overlay atomically:atomically];
Expand Down Expand Up @@ -202,4 +204,115 @@ - (void)decompress:(ResponseBlock)completion {
}
}

- (NSData *)zipData {
if (_zipData) return _zipData;

if (self.overlay) {
NSString *tmpDir = SKUniqueIdentifier();
NSString *videoName = [@"media~zip-" stringByAppendingString:SKUniqueIdentifier().capitalizedString];
NSString *overlayName = [@"overlay~zip-" stringByAppendingString:SKUniqueIdentifier().capitalizedString];
NSString *folder = [SKTempDirectory() stringByAppendingPathComponent:tmpDir];
NSString *archive = [SKTempDirectory() stringByAppendingPathComponent:[tmpDir stringByAppendingString:@".zip"]];
NSString *video = [folder stringByAppendingPathComponent:videoName];
NSString *overlay = [folder stringByAppendingPathComponent:overlayName];

[[NSFileManager defaultManager] createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:nil error:nil];
[self.data writeToFile:video atomically:YES];
[self.overlay writeToFile:overlay atomically:YES];
BOOL success = [SSZipArchive createZipFileAtPath:archive withContentsOfDirectory:folder];

if (success) {
_zipData = [NSData dataWithContentsOfFile:archive];
// zip failed
if (_zipData.length == 22) _zipData = nil;
}

// Cleanup
[[NSFileManager defaultManager] removeItemAtPath:archive error:nil];
[[NSFileManager defaultManager] removeItemAtPath:folder error:nil];
}

return _zipData;
}

- (NSData *)videoThumbnail {
if (_videoThumbnail) return _videoThumbnail;

if (self.isVideo) {
NSString *path = [SKTempDirectory() stringByAppendingPathComponent:@"tmp-video.mp4"];
[self.data writeToFile:path atomically:YES];
AVURLAsset *asset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:path]];

AVAssetImageGenerator *gen = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];

CGImageRef image = [gen copyCGImageAtTime:CMTimeMake(0, 600) actualTime:NULL error:NULL];
if (image != NULL) {
_videoThumbnail = SKThumbnailFromGCImage(image);
CGImageRelease(image);
}

// Cleanup
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}

return _videoThumbnail;
}

@end



NSData * SKThumbnailFromGCImage(CGImageRef image) {
CGFloat width = CGImageGetWidth(image);
CGFloat height = CGImageGetHeight(image);
CGFloat s = MIN(MIN(width, height), 102);

// Images too small to scale
if (s < 102) {
CGFloat cx = s/2.f - 51;
CGFloat cy = s/2.f - 51;
CGImageRef cropped = CGImageCreateWithImageInRect(image, CGRectMake(cx, cy, s, s));
return CFBridgingRelease(CGDataProviderCopyData(CGImageGetDataProvider(cropped)));
}

CGFloat scale = MIN(width, height) / 102.f;
NSInteger bitsPerComponent = CGImageGetBitsPerComponent(image);
NSInteger bytesPerRow = CGImageGetBytesPerRow(image);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
width /= scale; height /= scale;
CGFloat cx = (width - s) / 2.f;
CGFloat cy = (height - s) / 2.f;

CGContextRef context = CGBitmapContextCreate(nil, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo);

CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
CGImageRef scaled = CGBitmapContextCreateImage(context);
CGImageRef cropped = CGImageCreateWithImageInRect(scaled, CGRectMake(cx, cy, s, s));

// Cleanup
CGImageRelease(scaled);
NSData *ret;
#if __has_include(<UIKit/UIImage.h>)
ret = UIImagePNGRepresentation([UIImage imageWithCGImage:cropped]);
#elif __has_include(<AppKit/NSImage.h>)
ret = [[[NSBitmapImageRep alloc] initWithCGImage:cropped] representationUsingType:NSPNGFileType properties:@{}];
#else
#warning Target platform is missing a way to convert a CGImage to NSData.
#endif
CGImageRelease(cropped);

return ret;
}











6 changes: 4 additions & 2 deletions Pod/Classes/Model/SKRequest.m
Expand Up @@ -152,13 +152,15 @@ - (id)initWithPOSTEndpoint:(NSString *)endpoint query:(NSDictionary *)params hea

// Set HTTPBody
// Only for uploading snaps here
if ([endpoint isEqualToString:SKEPSnaps.upload] || [endpoint isEqualToString:SKEPAccount.avatar.set]) {
if ([endpoint isEqualToString:SKEPSnaps.upload] ||
[endpoint isEqualToString:SKEPAccount.avatar.set] ||
[endpoint isEqualToString:SKEPStories.post]) {
[self setValue:@"multipart/form-data; boundary=Boundary+0xAbCdEfGbOuNdArY" forHTTPHeaderField:SKHeaders.contentType];
NSMutableData *body = [NSMutableData data];
[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", SKConsts.boundary] dataUsingEncoding:NSUTF8StringEncoding]];

for (NSString *key in json.allKeys) {
if ([key isEqualToString:@"data"]) {
if ([key isEqualToString:@"data"] || [key isEqualToString:@"thumbnail_data"]) {
[body appendData:[NSData boundaryWithKey:key forDataValue:json[key]]];
} else {
[body appendData:[NSData boundaryWithKey:key forStringValue:(NSString *)json[key]]];
Expand Down
2 changes: 1 addition & 1 deletion Pod/Classes/Model/SKStoryOptions.h
Expand Up @@ -21,7 +21,7 @@
@property (nonatomic) NSString *text;
/** Defaults to \c NO. */
@property (nonatomic) BOOL cameraFrontFacing;
/** Defaults to 3. */
/** Defaults to 3. Ignored for videos. */
@property (nonatomic) NSTimeInterval timer;

@end
6 changes: 3 additions & 3 deletions Pod/Classes/Networking/SKClient+Snaps.m
Expand Up @@ -45,7 +45,7 @@ - (void)sendSnap:(SKBlob *)blob options:(SKSnapOptions *)options completion:(Res
@"recipient_ids": options.recipients.recipientsString,
@"reply": @(options.isReply),
@"time": @((NSUInteger)options.timer),
@"zipped": @0,
@"zipped": blob.zipData ? @1 : @0,
@"username": self.username};
[self postTo:SKEPSnaps.send query:query callback:^(NSDictionary *json, NSError *sendError) {
if (!sendError) {
Expand All @@ -65,8 +65,8 @@ - (void)uploadSnap:(SKBlob *)blob completion:(ResponseBlock)completion {

NSDictionary *query = @{@"media_id": uuid,
@"type": blob.isImage ? @(SKMediaKindImage) : @(SKMediaKindVideo),
@"data": blob.data,
@"zipped": @0,
@"data": blob.zipData ? blob.zipData : blob.data,
@"zipped": blob.zipData ? @1 : @0,
@"features_map": @"{}",
@"username": self.username};

Expand Down
2 changes: 1 addition & 1 deletion Pod/Classes/Networking/SKClient+Stories.h
Expand Up @@ -22,7 +22,7 @@
- (void)postStory:(SKBlob *)blob options:(SKStoryOptions *)options completion:(ErrorBlock)completion;
/** Posts a story with the given options.
@param blob The \c SKBlob object containing the image or video data to send. Can be created with any \c NSData object.
@param duration The length of the story.
@param duration The length of the story. This value is ignored for video snaps.
@param completion Takes an error, if any.
@note Assumes camera not front facing. */
- (void)postStory:(SKBlob *)blob for:(NSTimeInterval)duration completion:(ErrorBlock)completion;
Expand Down
40 changes: 24 additions & 16 deletions Pod/Classes/Networking/SKClient+Stories.m
Expand Up @@ -31,22 +31,30 @@ - (void)postStory:(SKBlob *)blob options:(SKStoryOptions *)options completion:(E

[self uploadStory:blob completion:^(NSString *mediaID, NSError *error) {
if (!error) {
NSDictionary *query = @{@"caption_text_display": options.text,
@"story_timestamp": [NSString timestamp],
@"type": blob.isImage ? @(SKMediaKindImage) : @(SKMediaKindVideo),
@"media_id": mediaID,
@"client_id": mediaID,
@"time": @((NSUInteger)options.timer),
@"username": self.username,
@"camera_front_facing": @(options.cameraFrontFacing),
@"my_story": @"true",
@"zipped": @0,
@"shared_ids": @"{}"};
NSMutableDictionary *query = @{@"camera_front_facing": @(options.cameraFrontFacing),
@"client_id": mediaID,
@"filter_id": @"",
@"media_id": mediaID,
@"orientation": @"0",
@"story_timestamp": [NSString timestamp],
@"time": @((NSUInteger)options.timer),
@"type": blob.isImage ? @(SKMediaKindImage) : @(SKMediaKindVideo),
@"username": self.username,
// @"my_story": @"true",
@"zipped": blob.zipData ? @1 : @0}.mutableCopy;
// Optional parts
if (options.text) {
query[@"caption_text_display"] = options.text;
}
if (blob.videoThumbnail) {
query[@"thumbnail_data"] = blob.videoThumbnail;
}

[self postTo:SKEPStories.post query:query callback:^(NSDictionary *json, NSError *sendError) {
completion(sendError);
SKRunBlockP(completion, sendError);
}];
} else {
completion(error);
SKRunBlockP(completion, error);
}
}];
}
Expand All @@ -56,13 +64,13 @@ - (void)uploadStory:(SKBlob *)blob completion:(ResponseBlock)completion {

NSDictionary *query = @{@"media_id": uuid,
@"type": blob.isImage ? @(SKMediaKindImage) : @(SKMediaKindVideo),
@"data": blob.data,
@"zipped": @0,
@"data": blob.zipData ? blob.zipData : blob.data,
@"zipped": blob.zipData ? @1 : @0,
@"features_map": @"{}",
@"username": self.username};

[self postTo:SKEPStories.upload query:query callback:^(id object, NSError *error) {
completion(error ? nil : uuid, error);
SKRunBlockP(completion, error ? nil : uuid, error);
}];
}

Expand Down
4 changes: 4 additions & 0 deletions Pod/Classes/SnapchatKit-Constants.h
Expand Up @@ -26,6 +26,10 @@
#define SK_NAMESPACE(name, vals) extern const struct name vals name
#define SK_NAMESPACE_IMP(name) const struct name name =

#define SKRunBlock(block) if ( block ) block()
#define SKRunBlockP(block, ...) if ( block ) block( __VA_ARGS__ )


typedef void (^RequestBlock)(NSData *data, NSURLResponse *response, NSError *error);
typedef void (^BooleanBlock)(BOOL success, NSError *error);
typedef void (^DataBlock)(NSData *data, NSError *error);
Expand Down
6 changes: 3 additions & 3 deletions Pod/Classes/SnapchatKit-Constants.m
Expand Up @@ -260,16 +260,16 @@ BOOL SKMediaKindIsVideo(SKMediaKind mediaKind) {

#pragma mark SKEPSnaps
SK_NAMESPACE_IMP(SKEPSnaps) {
.loadBlob = @"/bq/blob", // /ph/blob ?
.upload = @"/ph/upload",
.loadBlob = @"/bq/blob",
.upload = @"/bq/upload",
.send = @"/loq/retry",
.retry = @"/loq/send"
};

#pragma mark SKEPStories
SK_NAMESPACE_IMP(SKEPStories) {
.stories = @"/bq/stories",
.upload = @"/ph/upload",
.upload = @"/bq/upload",
.blob = @"/bq/story_blob?story_id=",
.thumb = @"/bq/story_thumbnail?story_id=",
.authBlob = @"/bq/auth_story_blob",
Expand Down
32 changes: 16 additions & 16 deletions SnapchatKit.podspec
@@ -1,20 +1,20 @@
Pod::Spec.new do |s|
s.name = "SnapchatKit"
s.version = "0.6.0"
s.summary = "An Objective-C implementation of the unofficial Snapchat API."
s.homepage = "https://github.com/ThePantsThief/SnapchatKit"
s.license = 'MIT'
s.author = { "ThePantsThief" => "tannerbennett@me.com" }
s.source = { :git => "https://github.com/ThePantsThief/SnapchatKit.git", :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/ThePantsThief'
s.name = "SnapchatKit"
s.version = "0.6.1"
s.summary = "An Objective-C implementation of the unofficial Snapchat API."
s.homepage = "https://github.com/ThePantsThief/SnapchatKit"
s.license = 'MIT'
s.author = { "ThePantsThief" => "tannerbennett@me.com" }
s.source = { :git => "https://github.com/ThePantsThief/SnapchatKit.git", :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/ThePantsThief'

s.requires_arc = true
s.ios.deployment_target = '7.0'
s.osx.deployment_target = '10.8'
s.requires_arc = true
s.ios.deployment_target = '7.0'
s.osx.deployment_target = '10.8'

s.source_files = 'Pod/Classes/*', 'Pod/Classes/**/*', 'Pod/Dependencies/*', 'Pod/Dependencies/**/*'
# s.dependency 'AFNetworking', '~> 2.5'
# s.dependency 'SSZipArchive'
s.dependency 'Mantle', '~> 2.0'
s.library = 'z'
s.source_files = 'Pod/Classes/*', 'Pod/Classes/**/*', 'Pod/Dependencies/*', 'Pod/Dependencies/**/*'
# s.dependency 'AFNetworking', '~> 2.5'
# s.dependency 'SSZipArchive'
s.dependency 'Mantle', '~> 2.0'
s.library = 'z'
end

0 comments on commit 2709bdc

Please sign in to comment.