Skip to content

Commit

Permalink
[New feature] Add an ability to extract frame images from a video file
Browse files Browse the repository at this point in the history
  • Loading branch information
metasmile committed Aug 3, 2016
1 parent 9fd87d1 commit a421223
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 84 deletions.
13 changes: 3 additions & 10 deletions DemoProject/NSGIF-OSX/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="15A263e" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15G31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="7706"/>
<plugIn identifier="com.apple.WebKitIBPlugin" version="7706"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
<plugIn identifier="com.apple.WebKitIBPlugin" version="10117"/>
</dependencies>
<scenes>
<!--Application-->
Expand Down Expand Up @@ -679,7 +679,6 @@
<constraint firstAttribute="width" constant="101" id="ZNc-da-bMV"/>
<constraint firstAttribute="height" constant="21" id="x9d-yU-JVp"/>
</constraints>
<animations/>
<buttonCell key="cell" type="push" title="Demo video" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="lht-fT-QYv">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
Expand All @@ -693,7 +692,6 @@
<constraints>
<constraint firstAttribute="height" constant="22" id="sJj-nj-yY9"/>
</constraints>
<animations/>
<textFieldCell key="cell" lineBreakMode="charWrapping" selectable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="File path" drawsBackground="YES" id="2zV-Jb-JD1">
<font key="font" metaFont="system"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
Expand All @@ -706,7 +704,6 @@
<constraint firstAttribute="width" constant="88" id="XC5-S9-B5Y"/>
<constraint firstAttribute="height" constant="21" id="x6C-9N-YpX"/>
</constraints>
<animations/>
<buttonCell key="cell" type="push" title="Select file" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="lYd-7d-lJt">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
Expand All @@ -721,15 +718,13 @@
<constraint firstAttribute="width" constant="16" id="6kq-TL-tlp"/>
<constraint firstAttribute="height" constant="16" id="Cvm-0H-tgz"/>
</constraints>
<animations/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1Rj-9X-X4B">
<rect key="frame" x="228" y="276" width="85" height="17"/>
<constraints>
<constraint firstAttribute="width" constant="81" id="jH0-24-rI7"/>
<constraint firstAttribute="height" constant="17" id="kWh-9N-hqm"/>
</constraints>
<animations/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Resulting GIF" id="VAG-RO-P2V">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
Expand All @@ -738,7 +733,6 @@
</textField>
<webView translatesAutoresizingMaskIntoConstraints="NO" id="SCu-ql-VVX">
<rect key="frame" x="20" y="11" width="501" height="257"/>
<animations/>
<webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12" plugInsEnabled="NO" javaEnabled="NO">
<nil key="identifier"/>
</webPreferences>
Expand All @@ -761,7 +755,6 @@
<constraint firstAttribute="trailing" secondItem="g4D-oc-SN5" secondAttribute="trailing" constant="20" id="pyI-lr-cKi"/>
<constraint firstItem="g4D-oc-SN5" firstAttribute="top" secondItem="8NR-BF-woH" secondAttribute="bottom" constant="13" id="xlf-Lv-SKX"/>
</constraints>
<animations/>
</view>
<connections>
<outlet property="activityIndicator" destination="ptr-xt-kan" id="yYR-0i-2Q1"/>
Expand Down
18 changes: 17 additions & 1 deletion DemoProject/NSGIF/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
Expand Down Expand Up @@ -55,16 +55,31 @@
<constraint firstAttribute="width" constant="37" id="kZd-EC-vzQ"/>
</constraints>
</activityIndicatorView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pvU-lT-ktX">
<rect key="frame" x="76" y="417" width="224" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="9CC-Sn-T1F"/>
</constraints>
<state key="normal" title="Extract Frames from demo video">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="button3Tapped:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="6Ui-kW-0Ct"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="yvE-4j-Onc" secondAttribute="trailing" id="1wU-to-aIQ"/>
<constraint firstItem="Enu-cu-ZuI" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="239" id="DJq-wk-22f"/>
<constraint firstItem="ggv-an-eqq" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="74" id="Ety-XG-Cvw"/>
<constraint firstAttribute="centerX" secondItem="1Pe-Wl-Z2T" secondAttribute="centerX" id="Ev1-RI-ztb"/>
<constraint firstItem="pvU-lT-ktX" firstAttribute="top" secondItem="ggv-an-eqq" secondAttribute="bottom" constant="8" symbolic="YES" id="JBi-D7-DLB"/>
<constraint firstItem="ggv-an-eqq" firstAttribute="top" secondItem="Enu-cu-ZuI" secondAttribute="bottom" constant="50" id="QAq-Uf-UdI"/>
<constraint firstItem="yvE-4j-Onc" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" id="Xw1-8a-c4o"/>
<constraint firstAttribute="centerX" secondItem="Enu-cu-ZuI" secondAttribute="centerX" id="c7g-VQ-j40"/>
<constraint firstItem="pvU-lT-ktX" firstAttribute="centerX" secondItem="ggv-an-eqq" secondAttribute="centerX" id="cE0-z0-euI"/>
<constraint firstAttribute="trailingMargin" secondItem="ggv-an-eqq" secondAttribute="trailing" constant="74" id="iE4-Hm-q7G"/>
<constraint firstAttribute="centerX" secondItem="ggv-an-eqq" secondAttribute="centerX" id="k3k-V7-0UR"/>
<constraint firstItem="Enu-cu-ZuI" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="74" id="xAD-ja-hHA"/>
Expand All @@ -79,6 +94,7 @@
<outlet property="activityIndicator" destination="1Pe-Wl-Z2T" id="Fij-1H-Xka"/>
<outlet property="button1" destination="Enu-cu-ZuI" id="aSv-c2-nep"/>
<outlet property="button2" destination="ggv-an-eqq" id="gpq-nQ-6cp"/>
<outlet property="button3" destination="pvU-lT-ktX" id="2AD-XJ-PSB"/>
<outlet property="webView" destination="yvE-4j-Onc" id="zlr-Ub-Oe9"/>
</connections>
</viewController>
Expand Down
1 change: 1 addition & 0 deletions DemoProject/NSGIF/ViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

@property (strong, nonatomic) IBOutlet UIButton *button1;
@property (strong, nonatomic) IBOutlet UIButton *button2;
@property (strong, nonatomic) IBOutlet UIButton *button3;

@property (strong, nonatomic) IBOutlet UIWebView *webView;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
Expand Down
46 changes: 44 additions & 2 deletions DemoProject/NSGIF/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingM
[self.activityIndicator startAnimating];
self.button1.enabled = NO;
self.button2.enabled = NO;

self.button3.enabled = NO;

NSGIFRequest * request = [NSGIFRequest requestWithSourceVideo:url];
request.progressHandler = ^(double progress, NSUInteger position, NSUInteger length, CMTime time, BOOL *stop, NSDictionary *frameProperties) {
NSLog(@"%f - %lu - %lu - %lld - %@", progress, position, length, time.value, frameProperties);
Expand All @@ -42,6 +42,7 @@ -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingM
[UIView animateWithDuration:0.3 animations:^{
self.button1.alpha = 0.0f;
self.button2.alpha = 0.0f;
self.button3.alpha = 0.0f;
self.webView.alpha = 1.0f;
}];
[self.webView loadRequest:[NSURLRequest requestWithURL:GifURL]];
Expand Down Expand Up @@ -70,6 +71,7 @@ - (IBAction)button1Tapped:(id)sender {
[self.activityIndicator startAnimating];
self.button1.enabled = NO;
self.button2.enabled = NO;
self.button3.enabled = NO;

NSURL *videoURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"video" ofType:@"mp4"]];

Expand All @@ -80,6 +82,7 @@ - (IBAction)button1Tapped:(id)sender {
[UIView animateWithDuration:0.3 animations:^{
self.button1.alpha = 0.0f;
self.button2.alpha = 0.0f;
self.button3.alpha = 0.0f;
self.webView.alpha = 1.0f;
}];
[self.webView loadRequest:[NSURLRequest requestWithURL:GifURL]];
Expand Down Expand Up @@ -107,4 +110,43 @@ - (IBAction)button2Tapped:(id)sender {
});
}

- (IBAction)button3Tapped:(id)sender {

[self.activityIndicator startAnimating];
self.button1.enabled = NO;
self.button2.enabled = NO;
self.button3.enabled = NO;

NSURL *videoURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"video" ofType:@"mp4"]];

UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];

NSFrameExtractingFromVideoRequest * request = [NSFrameExtractingFromVideoRequest requestWithSourceVideo:videoURL];
request.progressHandler = ^(double progress, NSUInteger offset, NSUInteger length, CMTime time, BOOL *stop, NSDictionary *frameProperties) {
NSLog(@"Progress: %@, %@, %@", [@(progress) stringValue], [@(offset) stringValue], [@(time.value) stringValue]);
};
request.frameCount = 10;

[NSGIF extract:request completion:^(NSArray<NSURL *> *extractedFrameImageUrls) {
NSLog(@"Finished generating frames: %@", extractedFrameImageUrls);

[self.activityIndicator stopAnimating];
[UIView animateWithDuration:0.3 animations:^{
self.button1.alpha = 0.0f;
self.button2.alpha = 0.0f;
self.button3.alpha = 0.0f;
self.webView.alpha = 1.0f;
}];

for(NSURL * imageUrl in extractedFrameImageUrls){
UIView * imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:imageUrl.path]];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.frame = CGRectMake(0,((CGFloat)[extractedFrameImageUrls indexOfObject:imageUrl])*30, 30,30);
[scrollView addSubview:imageView];
}
}];

[self.view addSubview:scrollView];
}

@end
12 changes: 11 additions & 1 deletion DemoProject/NSGIF/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
// main.m
// NSGIF
//
// Created by Sebastian Dobrincu on 30/08/15.
// Created by Metasmile on 30/08/15.
// Copyright (c) 2015 Sebastian Dobrincu. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#if DEBUG
void uncaughtExceptionHandler(NSException *exception) {
NSLog(@"Exception: %@", exception);
NSLog(@"Stack Trace: %@", [exception callStackSymbols]);
}
#endif

int main(int argc, char * argv[]) {
@autoreleasepool {
#if DEBUG
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
#endif
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
61 changes: 42 additions & 19 deletions NSGIF/NSGIF.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,21 @@ typedef NS_ENUM(NSInteger, NSGIFScale) {

typedef void (^ NSGIFProgressHandler)(double progress, NSUInteger offset, NSUInteger length, CMTime time, BOOL *__nullable stop, NSDictionary *__nullable frameProperties);

@interface NSGIFRequest : NSObject

#pragma mark NSSerializedAssetRequest
@interface NSSerializedResourceRequest : NSObject
/* required.
* a file's url of source video */
@property(nullable, nonatomic) NSURL * sourceVideoFile;

/* optional.
* defaults to nil.
* automatically assign the file name of source video (ex: IMG_0000.MOV -> IMG_0000.gif) */
@property(nullable, nonatomic) NSURL * destinationVideoFile;

/* optional but important.
* Defaults to NSGIFScaleOptimize (not set).
* This option will affect gif file size, memory usage and processing speed. */
@property(nonatomic, assign) NSGIFScale scalePreset;

/* optional.
* Defaults is to not set. unit is seconds, which means unlimited */
@property(nonatomic, assign) NSTimeInterval maxDuration;

/* optional but important.
* Defaults to 4.
* number of frames in seconds.
Expand All @@ -56,6 +55,27 @@ typedef void (^ NSGIFProgressHandler)(double progress, NSUInteger offset, NSUInt
* How far along the video track we want to move, in seconds. It will automatically assign from duration of video and framesPerSecond. */
@property(nonatomic, assign) NSUInteger frameCount;

/* optional.
* Defaults is nil */
@property (nonatomic, copy, nullable) NSGIFProgressHandler progressHandler;

/* readonly
* status for gif creating job 'YES' equals to 'now proceeding'
*/
@property(atomic, readonly) BOOL proceeding;

- (instancetype __nonnull)initWithSourceVideo:(NSURL * __nullable)fileURL;
+ (instancetype __nonnull)requestWithSourceVideo:(NSURL * __nullable)fileURL;
@end

#pragma mark NSGIFRequest
@interface NSGIFRequest : NSSerializedResourceRequest

/* optional.
* defaults to nil.
* automatically assign the file name of source video (ex: IMG_0000.MOV -> IMG_0000.gif) */
@property(nullable, nonatomic) NSURL * destinationVideoFile;

/* optional.
* Defaults to 0,
* the number of times the GIF will repeat. which means repeat infinitely. */
Expand All @@ -67,24 +87,27 @@ typedef void (^ NSGIFProgressHandler)(double progress, NSUInteger offset, NSUInt
* This option will NOT affect gif file size, memory usage and processing speed. It affect only FPS. */
@property(nonatomic, assign) CGFloat delayTime;

/* optional.
* Defaults is to not set. unit is seconds, which means unlimited */
@property(nonatomic, assign) NSTimeInterval maxDuration;
+ (NSGIFRequest * __nonnull)requestWithSourceVideo:(NSURL * __nullable)fileURL destination:(NSURL * __nullable)videoFileURL;
+ (NSGIFRequest * __nonnull)requestWithSourceVideoForLivePhoto:(NSURL *__nullable)fileURL;
@end

#pragma mark NSExtractFramesRequest
@interface NSFrameExtractingFromVideoRequest : NSSerializedResourceRequest
/* optional.
* Defaults is nil */
@property (nonatomic, copy, nullable) NSGIFProgressHandler progressHandler;

/* gif creation job is now proceeding */
@property(atomic, readonly) BOOL proceeding;
* Defaults to jpg.
* This property will be affect to UTType(Automatically detected) of extracting image file.
*/
@property(nonatomic, readwrite, nullable) NSString * extension;

- (NSGIFRequest * __nonnull)initWithSourceVideo:(NSURL * __nullable)fileURL;
+ (NSGIFRequest * __nonnull)requestWithSourceVideo:(NSURL * __nullable)fileURL;
+ (NSGIFRequest * __nonnull)requestWithSourceVideo:(NSURL * __nullable)fileURL destination:(NSURL * __nullable)videoFileURL;
+ (NSGIFRequest * __nonnull)requestWithSourceVideoForLivePhoto:(NSURL *__nullable)fileURL;
/* optional.
* defaults to temp directory.
*/
@property(nullable, nonatomic) NSURL * destinationDirectory;
@end

@interface NSGIF : NSObject

+ (void)create:(NSGIFRequest *__nullable)request completion:(void (^ __nullable)(NSURL * __nullable))completionBlock;

+ (void)extract:(NSFrameExtractingFromVideoRequest *__nullable)request completion:(void (^ __nullable)(NSArray<NSURL *> * __nullable))completionBlock;
@end
Loading

0 comments on commit a421223

Please sign in to comment.