OC runtime UIButton扩展Block点击事件

From: http://www.jianshu.com/p/d4eab6815ac2

主要实现通过runtime动态关联属性。
上码!!!!

#import <UIKit/UIKit.h>

typedef void(^ButtonBlock)(UIButton* btn);

@interface UIButton (Block)

/**
 *  button 添加点击事件
 *
 *  @param block
 */
- (void)addAction:(ButtonBlock)block;

/**
 *  button 添加事件
 *
 *  @param block
 *  @param controlEvents 点击的方式
 */
- (void)addAction:(ButtonBlock)block forControlEvents:(UIControlEvents)controlEvents;

@end

////////////////////////////////////////////////////


#import "UIButton+Block.h"

#import <objc/runtime.h>

@implementation UIButton (Block)
static char ActionTag;

/**
 *  button 添加点击事件 默认点击方式UIControlEventTouchUpInside
 *
 *  @param block
 */
- (void)addAction:(ButtonBlock)block {
    objc_setAssociatedObject(self, &ActionTag, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self addTarget:self action:@selector(action:) forControlEvents:UIControlEventTouchUpInside];
}

/**
 *  button 添加事件
 *
 *  @param block
 *  @param controlEvents 点击的方式
 */
- (void)addAction:(ButtonBlock)block forControlEvents:(UIControlEvents)controlEvents {
    objc_setAssociatedObject(self, &ActionTag, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self addTarget:self action:@selector(action:) forControlEvents:controlEvents];
}

/**
 *  button 事件的响应方法
 *
 *  @param sender 
 */
- (void)action:(id)sender {
    ButtonBlock blockAction = (ButtonBlock)objc_getAssociatedObject(self, &ActionTag);
    if (blockAction) {
        blockAction(self);
    }
}
@end

使用方法如下

UIButton *testButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
testButton.backgroundColor = [UIColor redColor];
[self.view addSubview:testButton];

[testButton addAction:^(UIButton *btn) {
    NSLog(@"我被点名了");
}];

[testButton addAction:^(UIButton *btn) {
     NSLog(@"我被点名了");
} forControlEvents:UIControlEventTouchUpInside];

各种进度提示layer 动画

From: http://www.code4app.com/blog-822721-738.html

初步学习了CoreAnimation框架,总结了几个动画效果,主要是通过CAShapeLayer与贝塞尔曲线实现。先看下效果

1.gif

扇形下载进度

要实现扇形的下载进度,有两种方法, 这里先使用第一种:

1.使用设置UIBezierPath的角度

2.使用 CAShapeLayer的stokeEnd属性

CGPoint point = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);


CGFloat startAngle = - M_PI /2;


CGFloat endAngle = self.progress *M_PI *2 + startAngle;


UIBezierPath *path =[UIBezierPath bezierPathWithArcCenter:point radius:self.bounds.size.width/2 startAngle:startAngle endAngle:endAngle clockwise:1];


[path addLineToPoint:point];



CAShapeLayer *layer =[CAShapeLayer layer];


layer.path = path.CGPath;

layer.fillColor =[UIColor colorWithRed:0.47 green:0.83 blue:0.98 alpha:1].CGColor;

[self.layer addSublayer:layer];

圆形进度

首先 我们需要一个背景层 一个前景层,一个路径供给两个layer使用。这里我们使用改变stokeEnd 来改变圆弧的进度,代码里增加了一点渐变
   self.backLayer =[CAShapeLayer layer];
    self.backLayer.fillColor  =[UIColor clearColor].CGColor;
    self.backLayer.frame = self.bounds;
    self.backLayer.lineWidth = 4;
    self.backLayer.strokeColor =[UIColor lightGrayColor].CGColor;
    [self.layer addSublayer:self.backLayer];

    self.foreLayer =[CAShapeLayer layer];
    self.foreLayer.fillColor  =[UIColor clearColor].CGColor;
    self.foreLayer.frame = self.bounds;
    self.foreLayer.strokeColor =[UIColor redColor].CGColor;
    self.foreLayer.lineWidth = 4;
    [self.layer addSublayer:self.foreLayer];


    UIBezierPath *path=  [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-2 startAngle:-M_PI_2 endAngle:M_PI *1.5 clockwise:YES];
    self.backPath = path;

    self.backLayer.path = self.backPath.CGPath;
    self.foreLayer.path = self.backPath.CGPath;
    self.foreLayer.strokeEnd = 0;


    self.gradientLayerLeft =[CAGradientLayer layer];
    self.gradientLayerLeft.frame = self.bounds;

    self.gradientLayerLeft.colors =@[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
    self.gradientLayerLeft.locations = @[@0,@0.5,@1];
    self.gradientLayerLeft.startPoint = CGPointMake(0, 0);
    self.gradientLayerLeft.endPoint = CGPointMake(0, 1);
    [self.layer addSublayer:self.gradientLayerLeft];



    [self.gradientLayerLeft setMask:self.foreLayer];
-(void)setProgressValue:(CGFloat)progressValue
{
    _progressValue = progressValue;
    self.foreLayer.strokeEnd = progressValue;
    self.label.text = [NSString stringWithFormat:@"%.f%%",progressValue *100];
}

一个加载动画

引自:旋转加载动画

可以重点学习下做动画的思路,

- (void)animationDidStart:(CAAnimation *)anim{
    [UIView animateWithDuration:0.3 delay:0.1 options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionBeginFromCurrentState animations:^{

        self.ball_1.transform = CGAffineTransformMakeTranslation(-BALL_RADIUS, 0);
        self.ball_1.transform = CGAffineTransformScale(self.ball_1.transform, 0.7, 0.7);

        self.ball_3.transform = CGAffineTransformMakeTranslation(BALL_RADIUS, 0);
        self.ball_3.transform = CGAffineTransformScale(self.ball_3.transform, 0.7, 0.7);


        self.ball_2.transform = CGAffineTransformScale(self.ball_2.transform, 0.7, 0.7);
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.3 delay:0.1 options:UIViewAnimationOptionCurveEaseIn  | UIViewAnimationOptionBeginFromCurrentState animations:^{
            self.ball_1.transform = CGAffineTransformIdentity;
            self.ball_3.transform = CGAffineTransformIdentity;
            self.ball_2.transform = CGAffineTransformIdentity;
        } completion:NULL];

    }];
}

使用正余弦做的注水动画

在使用正余弦做注水动画时,先了解下正余弦

我们要做的 就是使用两条正余弦,但是这两条正余弦, 波峰需要对应波谷,有两种方法:

  1. 使用for循环分别拼接正余弦的路径

    -(void)updateWave
    {

    CGFloat waterWaveWidth = self.bounds.size.width;

    CGMutablePathRef path = CGPathCreateMutable();

    CGMutablePathRef maskPath = CGPathCreateMutable();

    CGPathMoveToPoint(path, nil, 0, _waveY);

    CGPathMoveToPoint(maskPath, nil, 0, _waveY);

    CGFloat y = _waveY;


    for (float x = 0.0f; x <= waterWaveWidth ; x++) {
        y = _waveAmplitude * sin(_wavePalstance * x + _waveX) + _waveY;

        CGPathAddLineToPoint(path, nil, x, y);

    }
    for (float x = 0.0f; x <= waterWaveWidth ; x++) {
        y = _waveAmplitude * cos(_wavePalstance * x + _waveX) + _waveY;

        CGPathAddLineToPoint(maskPath, nil, x, y);
    }
    [self updateLayer:_waveLayer1 path:path];
    [self updateLayer:_waveLayer2 path:maskPath];

}
-(void)updateLayer:(CAShapeLayer *)layer path:(CGMutablePathRef )path
{

    CGFloat waterWaveWidth = self.bounds.size.width;
    CGPathAddLineToPoint(path, nil, waterWaveWidth, self.bounds.size.height);
    CGPathAddLineToPoint(path, nil, 0, self.bounds.size.height);
    CGPathCloseSubpath(path);
    layer.path = path;
     CGPathRelease(path);
}

2.使用单个for循环只是 设置 另一条曲线的y值相反即可实现两条正余弦的效果 ,最后一个动画中会有说明

for (int x = 0; x<WIDTH; x++) {
        y = waveHeight*sinf(0.01*waveCurvature*x+offSetValue*0.045);
        CGPathAddLineToPoint(path, nil, x, y);

        masky = -y;
        CGPathAddLineToPoint(maskPath, nil, x, masky);
    }

但是我们有个需求就是改变 波浪的高度, 实现注水的百分比,就需要设置波浪的偏距

-(void)updateWaveY
{
    CGFloat targetY = self.bounds.size.height - _progress * self.bounds.size.height;
    if (_waveY < targetY) {
        _waveY += 2;
    }
    if (_waveY > targetY ) {
        _waveY -= 2;
    }
}

正余弦动画2

如果有个需求 ,比如一个小船 随着波浪的波动而起伏

那我们就需要计算 波浪的位置,然后设置小船的frame

    for (int x = 0; x<WIDTH; x++) {
        y = waveHeight*sinf(0.01*waveCurvature*x+offSetValue*0.045);
        CGPathAddLineToPoint(path, nil, x, y);

        masky = -y;
        CGPathAddLineToPoint(maskPath, nil, x, masky);
    }
计算出实浪波动时,最中间的位置,设置小船的frame
  CGFloat CentY = waveHeight*sinf(0.01*waveCurvature*WIDTH/2+offSetValue*0.045);
    CGRect boardViewFrame = [boardView frame];
    boardViewFrame.origin.y = 100-waveHeight+CentY;
    boardView.frame = boardViewFrame;

来自:https://github.com/liuxinixn/ProgressViewDemo

iOS音频开发(录音+播放+剪辑+合成+压缩转码)

iOS音频开发(录音+播放+剪辑+合成+压缩转码)

From: http://blog.csdn.net/feng2qing/article/details/67655175

录音:

AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *sessionError;

[session setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
[session setActive:YES error:nil];

NSDictionary *setting = [NSDictionary dictionaryWithObjectsAndKeys:
                               [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                               [NSNumber numberWithFloat:8000], AVSampleRateKey, 
                               [NSNumber numberWithInt:2], AVNumberOfChannelsKey, 
                               [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,  
                               [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,  
                               [NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,  
                               [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,  
                               [NSNumber numberWithInt:AVAudioQualityMax], AVEncoderAudioQualityKey,  
                               nil];
self.audioRecorder.delegate = self;

self.audioRecorder.meteringEnabled = YES;

self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:[NSURL URLWithString:filePath] settings:setting error:nil];

[self.audioRecorder prepareToRecord];
[self.audioRecorder record];


[self.audioRecorder pause];

[self.audioRecorder stop];




- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag;

- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError * __nullable)error;

- (void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder NS_DEPRECATED_IOS(2_2, 8_0);

- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);

播放

AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *sessionError;
[session setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
[session setActive:YES error:nil];


[[UIDevice currentDevice] setProximityMonitoringEnabled:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sensorStateChange:)name:UIDeviceProximityStateDidChangeNotification object:nil];

self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL URLWithString:filePath] error:nil];

self.audioPlayer.delegate = self;

[self.audioPlayer prepareToPlay];
[self.audioPlayer play];


[self.audioPlayer stop];

[self.audioPlayer pause];


if ([[UIDevice currentDevice] proximityState] == YES) {

    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
} else {

    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
}



- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;

- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error;

- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);

- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);

剪辑

//AVURLAsset是AVAsset的子类,AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息.而AVURLAsset子类的作用则是根据NSURL来初始化AVAsset对象.
AVURLAsset *videoAsset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:filePath]];
//音频输出会话
//AVAssetExportPresetAppleM4A: This export option will produce an audio-only .m4a file with appropriate iTunes gapless playback data(输出音频,并且是.m4a格式)
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:videoAsset presetName:AVAssetExportPresetAppleM4A];
//设置输出路径 / 文件类型 / 截取时间段
exportSession.outputURL = [NSURL fileURLWithPath:resultPath];
exportSession.outputFileType = AVFileTypeAppleM4A;
exportSession.timeRange = CMTimeRangeFromTimeToTime(CMTimeMake(time1, 1), CMTimeMake(time2, 1));
[exportSession exportAsynchronouslyWithCompletionHandler:^{
    //exporeSession.status
}];

合成

将路径filePath1和路径filePath2下的音频合成

AVURLAsset *videoAsset1 = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:filePath1] options:nil];
AVURLAsset *videoAsset2 = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:filePath2] options:nil];

AVAssetTrack *assetTrack1 = [[videoAsset1 tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVAssetTrack *assetTrack2 = [[videoAsset2 tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];

AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *compositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

[compositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset1.duration) ofTrack:assetTrack1 atTime:kCMTimeZero error:nil];
[compositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset2.duration) ofTrack:assetTrack2 atTime:videoAsset1.duration error:nil];

AVAssetExportSession *exporeSession = [AVAssetExportSession exportSessionWithAsset:composition presetName:AVAssetExportPresetAppleM4A];
exporeSession.outputFileType = AVFileTypeAppleM4A;
exporeSession.outputURL = [NSURL fileURLWithPath:resultPath];
[exporeSession exportAsynchronouslyWithCompletionHandler:^{

}];

压缩转码

下载LAME (Lame Aint an MP3 Encoder)

双击解压后放到一个文件夹下,文件夹需要命名为lame,否则无法生成.h.a文件

使用Terminal进入该文件夹,编译生成静态库,[脚本代码][3]

[3]: https://github.com/kewlbear/lame-ios-build/blob/master/build-lame.sh
1
2
3
4
5
6
7
$:cd cd /Users/mac/Desktop/lame
//创建build_lame.sh
$:touch build_lame.sh
//打开build_lame.sh,粘贴脚本代码
$:open build_lame.sh
//编译执行脚本,生成静态库,需要输入本机密码
$:sudo sh build_lame.sh

这里写图片描述

fat-lame文件夹下的include文件夹和lib文件夹放入工程,再写一个OC的类调用lame.h

@try {
    int read, write;
    FILE *pcm = fopen([filePath cStringUsingEncoding:1], "rb");
    fseek(pcm, 4*1024, SEEK_CUR);
    FILE *mp3 = fopen([resultPath cStringUsingEncoding:1], "wb");

    const int PCM_SIZE = 8192;
    const int MP3_SIZE = 8192;
    short int pcm_buffer[PCM_SIZE*2];
    unsigned char mp3_buffer[MP3_SIZE];


    lame_t lame = lame_init();

    lame_set_in_samplerate(lame, 8000);
    lame_set_num_channels(lame,2);
    lame_set_out_samplerate(lame, 8000);
    lame_set_brate(lame, 8);

    lame_set_quality(lame, 7);


    lame_set_VBR(lame, vbr_default);
    lame_init_params(lame);

    do {
        size_t size = (size_t)(2 * sizeof(short int));
        read = fread(pcm_buffer, size, PCM_SIZE, pcm);
        if (read == 0) {
            write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
        } else {
            write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
        }
        fwrite(mp3_buffer, write, 1, mp3);

    } while (read != 0);

    lame_close(lame);
    fclose(mp3);
    fclose(pcm);
}
@catch (NSException *exception) {
    NSLog(@"%@",[exception description]);
}
@finally {

    return resultPath;
}

基本上可以将100K左右的录音文件压缩到10K以下

参考1 参考2 参考3 参考4

swift 指针问题

虽然是老版本的但是参考意义也很不错
From: https://onevcat.com/2015/01/swift-pointer/

2015-01-19 • 能工巧匠集

Apple 期望在 Swift 中指针能够尽量减少登场几率,因此在 Swift 中指针被映射为了一个泛型类型,并且还比较抽象。这在一定程度上造成了在 Swift 中指针使用的困难,特别是对那些并不熟悉指针,也没有多少指针操作经验的开发者 (包括我自己也是) 来说,在 Swift 中使用指针确实是一个挑战。在这篇文章里,我希望能从最基本的使用开始,总结一下在 Swift 中使用指针的一些常见方式和场景。这篇文章假定你至少知道指针是什么,如果对指针本身的概念不太清楚的话,可以先看看这篇五分钟 C 指针教程 (或者它的中文版本),应该会很有帮助。

初步

在 Swift 中,指针都使用一个特殊的类型来表示,那就是 UnsafePointer<T>。遵循了 Cocoa 的一贯不可变原则,UnsafePointer<T> 也是不可变的。当然对应地,它还有一个可变变体,UnsafeMutablePointer<T>。绝大部分时间里,C 中的指针都会被以这两种类型引入到 Swift 中:C 中 const 修饰的指针对应 UnsafePointer (最常见的应该就是 C 字符串的 const char * 了),而其他可变的指针则对应 UnsafeMutablePointer。除此之外,Swift 中存在表示一组连续数据指针的 UnsafeBufferPointer<T>,表示非完整结构的不透明指针 COpaquePointer 等等。另外你可能已经注意到了,能够确定指向内容的指针类型都是泛型的 struct,我们可以通过这个泛型来对指针指向的类型进行约束以提供一定安全性。

对于一个 UnsafePointer<T> 类型,我们可以通过 memory 属性对其进行取值,如果这个指针是可变的 UnsafeMutablePointer<T> 类型,我们还可以通过 memory 对它进行赋值。比如我们想要写一个利用指针直接操作内存的计数器的话,可以这么做:

func incrementor(ptr: UnsafeMutablePointer<Int>) {
    ptr.memory += 1
}

var a = 10
incrementor(&a)

a  // 11

这里和 C 的指针使用类似,我们通过在变量名前面加上 & 符号就可以将指向这个变量的指针传递到接受指针作为参数的方法中去。在上面的 incrementor 中我们通过直接操作 memory 属性改变了指针指向的内容。

与这种做法类似的是使用 Swift 的 inout 关键字。我们在将变量传入 inout 参数的函数时,同样也使用 & 符号表示地址。不过区别是在函数体内部我们不需要处理指针类型,而是可以对参数直接进行操作。

func incrementor1(inout num: Int) {
    num += 1
}

var b = 10
incrementor1(&b)

b  // 11

虽然 & 在参数传递时表示的意义和 C 中一样,是某个“变量的地址”,但是在 Swift 中我们没有办法直接通过这个符号获取一个 UnsafePointer 的实例。需要注意这一点和 C 有所不同:

// 无法编译
let a = 100
let b = &a

指针初始化和内存管理

在 Swift 中不能直接取到现有对象的地址,我们还是可以创建新的 UnsafeMutablePointer 对象。与 Swift 中其他对象的自动内存管理不同,对于指针的管理,是需要我们手动进行内存的申请和释放的。一个 UnsafeMutablePointer 的内存有三种可能状态:

  • 内存没有被分配,这意味着这是一个 null 指针,或者是之前已经释放过
  • 内存进行了分配,但是值还没有被初始化
  • 内存进行了分配,并且值已经被初始化

其中只有第三种状态下的指针是可以保证正常使用的。UnsafeMutablePointer 的初始化方法 (init) 完成的都是从其他类型转换到 UnsafeMutablePointer 的工作。我们如果想要新建一个指针,需要做的是使用 alloc: 这个类方法。该方法接受一个 num: Int 作为参数,将向系统申请 num 个数的对应泛型类型的内存。下面的代码申请了一个 Int 大小的内存,并返回指向这块内存的指针:

var intPtr = UnsafeMutablePointer<Int>.alloc(1)
// "UnsafeMutablePointer(0x7FD3A8E00060)"

接下来应该做的是对这个指针的内容进行初始化,我们可以使用 initialize: 方法来完成初始化:

intPtr.initialize(10)
// intPtr.memory 为 10

在完成初始化后,我们就可以通过 memory 来操作指针指向的内存值了。

在使用之后,我们最好尽快释放指针指向的内容和指针本身。与 initialize: 配对使用的 destroy 用来销毁指针指向的对象,而与 alloc: 对应的 dealloc: 用来释放之前申请的内存。它们都应该被配对使用:

intPtr.destroy()
intPtr.dealloc(1)
intPtr = nil

注意其实在这里对于 Int 这样的在 C 中映射为 int 的 “平凡值” 来说,destroy 并不是必要的,因为这些值被分配在常量段上。但是对于像类的对象或者结构体实例来说,如果不保证初始化和摧毁配对的话,是会出现内存泄露的。所以没有特殊考虑的话,不论内存中到底是什么,保证 initialize:destroy 配对会是一个好习惯。

指向数组的指针

在 Swift 中将一个数组作为参数传递到 C API 时,Swift 已经帮助我们完成了转换,这在 Apple 的[官方博客][3]中有个很好的例子:

[3]: https://developer.apple.com/swift/blog/?id=6

import Accelerate

let a: [Float] = [1, 2, 3, 4]
let b: [Float] = [0.5, 0.25, 0.125, 0.0625]
var result: [Float] = [0, 0, 0, 0]

vDSP_vadd(a, 1, b, 1, &result, 1, 4)

// result now contains [1.5, 2.25, 3.125, 4.0625]

对于一般的接受 const 数组的 C API,其要求的类型为 UnsafePointer,而非 const 的数组则对应 UnsafeMutablePointer。使用时,对于 const 的参数,我们直接将 Swift 数组传入 (上例中的 ab);而对于可变的数组,在前面加上 & 后传入即可 (上例中的 result)。

对于传参,Swift 进行了简化,使用起来非常方便。但是如果我们想要使用指针来像之前用 memory 的方式直接操作数组的话,就需要借助一个特殊的类型:UnsafeMutableBufferPointer。Buffer Pointer 是一段连续的内存的指针,通常用来表达像是数组或者字典这样的集合类型。

var array = [1, 2, 3, 4, 5]
var arrayPtr = UnsafeMutableBufferPointer<Int>(start: &array, count: array.count)
// baseAddress 是第一个元素的指针
var basePtr = arrayPtr.baseAddress as UnsafeMutablePointer<Int>

basePtr.memory // 1
basePtr.memory = 10
basePtr.memory // 10

//下一个元素
var nextPtr = basePtr.successor()
nextPtr.memory // 2

指针操作和转换

withUnsafePointer

上面我们说过,在 Swift 中不能像 C 里那样使用 & 符号直接获取地址来进行操作。如果我们想对某个变量进行指针操作,我们可以借助 withUnsafePointer 这个辅助方法。这个方法接受两个参数,第一个是 inout 的任意类型,第二个是一个闭包。Swift 会将第一个输入转换为指针,然后将这个转换后的 Unsafe 的指针作为参数,去调用闭包。使用起来大概是这个样子:

var test = 10
test = withUnsafeMutablePointer(&test, { (ptr: UnsafeMutablePointer<Int>) -> Int in
    ptr.memory += 1
    return ptr.memory
})

test // 11

这里其实我们做了和文章一开始的 incrementor 相同的事情,区别在于不需要通过方法的调用来将值转换为指针。这么做的好处对于那些只会执行一次的指针操作来说是显而易见的,可以将“我们就是想对这个指针做点事儿”这个意图表达得更加清晰明确。

unsafeBitCast

unsafeBitCast 是非常危险的操作,它会将一个指针指向的内存强制按位转换为目标的类型。因为这种转换是在 Swift 的类型管理之外进行的,因此编译器无法确保得到的类型是否确实正确,你必须明确地知道你在做什么。比如:

let arr = NSArray(object: "meow")
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self)
str // “meow”

因为 NSArray 是可以存放任意 NSObject 对象的,当我们在使用 CFArrayGetValueAtIndex 从中取值的时候,得到的结果将是一个 UnsafePointer<Void>。由于我们很明白其中存放的是 String 对象,因此可以直接将其强制转换为 CFString

关于 unsafeBitCast 一种更常见的使用场景是不同类型的指针之间进行转换。因为指针本身所占用的的大小是一定的,所以指针的类型进行转换是不会出什么致命问题的。这在与一些 C API 协作时会很常见。比如有很多 C API 要求的输入是 void *,对应到 Swift 中为 UnsafePointer<Void>。我们可以通过下面这样的方式将任意指针转换为 UnsafePointer。

var count = 100
var voidPtr = withUnsafePointer(&count, { (a: UnsafePointer<Int>) -> UnsafePointer<Void> in
    return unsafeBitCast(a, UnsafePointer<Void>.self)
})
// voidPtr 是 UnsafePointer<Void>。相当于 C 中的 void *

// 转换回 UnsafePointer<Int>
var intPtr = unsafeBitCast(voidPtr, UnsafePointer<Int>.self)
intPtr.memory //100

总结

Swift 从设计上来说就是以安全作为重要原则的,虽然可能有些啰嗦,但是还是要重申在 Swift 中直接使用和操作指针应该作为最后的手段,它们始终是无法确保安全的。从传统的 C 代码和与之无缝配合的 Objective-C 代码迁移到 Swift 并不是一件小工程,我们的代码库肯定会时不时出现一些和 C 协作的地方。我们当然可以选择使用 Swift 重写部分陈旧代码,但是对于像是安全或者性能至关重要的部分,我们可能除了继续使用 C API 以外别无选择。如果我们想要继续使用那些 API 的话,了解一些基本的 Swift 指针操作和使用的知识会很有帮助。

对于新的代码,尽量避免使用 Unsafe 开头的类型,意味着可以避免很多不必要的麻烦。Swift 给开发者带来的最大好处是可以让我们用更加先进的编程思想,进行更快和更专注的开发。只有在尊重这种思想的前提下,我们才能更好地享受这门新语言带来的种种优势。显然,这种思想是不包括到处使用 UnsafePointer 的 :)

最近的文章

跨平台开发时代的 (再次) 到来?

这篇文章主要想谈谈最近又刮起的移动开发跨平台之风,并着重介绍和对比一下像是 Xamarin,NativeScript 和 React Native 之类的东西。不会有特别深入的技术讨论,大家可以当作一篇科普类的文章来看。故事的开始“一次编码,处处运行” 永远是程序员们的理想乡。二十年前 Java 正是举着这面大旗登场,击败了众多竞争对手。但是时至今日,事实已经证明了 Java 笨重的体型和缓慢的发展显然已经很难再抓住这个时代快速跃动的脚步。在新时代的移动大潮下,一个应用想要取胜,完美的使用……

2015-03-27 • 能工巧匠集继续阅读

更早的文章

Apple WatchKit 初探

随着今天凌晨 Apple 发布了第一版的 Watch Kit 的 API,对于开发者来说,这款新设备的一些更详细的信息也算是逐渐浮出水面。可以说第一版的 WatchKit 开放的功能总体还是令人满意的。Apple 在承诺逐渐开放的方向上继续前进。本来在 WWDC 之后预期 Today Widget 会是各类新颖 app 的舞台以及对 iOS 功能的极大扩展,但是随着像 Launcher 和 PCalc 这些创意型的 Today Widget 接连被下架事件,让开发者也不得不下调对 Watc……

2014-11-19 • 能工巧匠集继续阅读

本站点采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议Jekyll 于 2017-05-05 生成,感谢 Digital Ocean 为本站提供稳定的 VPS 服务 本站由 @onevcat 创建,采用 Vno - Jekyll 作为主题,您可以在 GitHub 找到本站源码 - © 2017

图片合成视频

https://github.com/czboosj/HJImagesToVideo 自己修复了这个库的内存泄漏

https://github.com/Willib/ImagesToVideo
swift 版本的图片处理

https://stackoverflow.com/questions/3741323/how-do-i-export-uiimage-array-as-a-movie?noredirect=1&lq=1
图片合成详解

From: http://www.itqls.com/index.php?m=Home&c=Article&a=index&id=63

最近做一个无线设备相关的项目,

需要把扫描得到的图像,合成视频.

由于扫描得到的图像 是通过 drawRect 绘图画出来的,不是一个 Image

而图片转视频的方法是需要用 CGImage去实现的.

方法如下:

由于我需要得到 Image 数组, 就需要把我绘制出来的图片转换一下,

截屏: (正确姿势)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (UIImage*)screenCap
{
    CGSize size=  CGSizeMake((int)self.bounds.size.width, (int)self.bounds.size.height);
    
//  size 为尺寸, YES 为不透明,第三个参数是缩放比例,直接根据屏幕设置保持清晰度
//  关于这个不透明好多博客都说错了,opaque 的意思是不透明...
    UIGraphicsBeginImageContextWithOptions(size, YES, [UIScreen mainScreen].scale);
// 遇到这个没拆分出来直接写,后发现内存泄漏,所以提出来 然后Relese
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:context];
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
CGContextRelease(context);
    UIGraphicsEndImageContext();
    return viewImage;
}

通过这个方法可以拿到当前屏幕的图像转换成的 Image

但是,如果需要拿到大量的图,比如100张,他就相当于去截图100张,

内存消耗会非常恐怖.

所以 调用 以上方法 screenCap 的时候, 在外面包一层 autoRelease 去解决.

图片转视频:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
- (void)saveVideo:(NSMutableArray *)imageArr withPaths:(NSString *)paths andCallBack:(void(^)(void))callBack
{
    if (!imageArr.count) {
        return;
    }
    
    UIImage *image = self.screenCap;
    
    CGSize sizeImage = image.size;
    int width = ((int) (sizeImage.width / 16) * 16);
    int height = ((int) (sizeImage.height / 16) * 16);
    NSLog(@"%d,%d",width,height);
    
    CGSize size = CGSizeMake(width, height);
    
    NSError *error = nil;
    unlink([paths UTF8String]);
    
    AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:
                                  [NSURL fileURLWithPath:paths]
                                fileType:AVFileTypeQuickTimeMovie
                                error:&error];
    
    NSParameterAssert(videoWriter);
    
    if(error)
        
        NSLog(@"error = %@", [error localizedDescription]);
    
    NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey,
                                   [NSNumber numberWithInt:size.width], AVVideoWidthKey,
                                   [NSNumber numberWithInt:size.height], AVVideoHeightKey, nil];
    
    AVAssetWriterInput *writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
    
    
    NSDictionary *sourcePixelBufferAttributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                                           
                                                           [NSNumber numberWithInt:kCVPixelFormatType_32ARGB], kCVPixelBufferPixelFormatTypeKey, nil];
    
    
    
    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput
                                                            sourcePixelBufferAttributes:sourcePixelBufferAttributesDictionary];
    
    NSParameterAssert(writerInput);
    
    NSParameterAssert([videoWriter canAddInput:writerInput]);
    
    if ([videoWriter canAddInput:writerInput])
        NSLog(@"I can add this input");
    else
        NSLog(@"i can't add this input");
    
    [videoWriter addInput:writerInput];
    
    [videoWriter startWriting];
    
    [videoWriter startSessionAtSourceTime:kCMTimeZero];
    dispatch_queue_t    dispatchQueue = dispatch_queue_create("mediaInputQueue", NULL);
    
    [writerInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
            
        CVPixelBufferRef buffer = NULL;
        NSUInteger fps = 10;
        int frameCount = 0;
        double numberOfSecondsPerFrame = 0.1;
        double frameDuration = fps * numberOfSecondsPerFrame;
        
        for (UIImage *img in imageArr)
        {
            //convert uiimage to CGImage.
            buffer = [self pixelBufferFromCGImage:[img CGImage] size:size];
            
            BOOL append_ok = NO;
            int j = 0;
            while (!append_ok && j < 30)
            {
                if (adaptor.assetWriterInput.readyForMoreMediaData)
                {
                    CMTime frameTime = CMTimeMake(frameCount * frameDuration, (int32_t) fps);
                    append_ok = [adaptor appendPixelBuffer:buffer withPresentationTime:frameTime];
                    if (!append_ok)
                    {
                        NSError *error = videoWriter.error;
                        if (error != nil)
                        {
                            NSLog(@"Unresolved error %@,%@.", error, [error userInfo]);
                        }
                    }
                }
                else
                {
                    printf("adaptor not ready %d, %d\n", frameCount, j);
                    [NSThread sleepForTimeInterval:0.1];
                }
                j++;
            }
            if (!append_ok)
            {
                printf("error appending image %d times %d\n, with error.", frameCount, j);
            }
            frameCount++;
            
            CVPixelBufferRelease(buffer);
        }
        //Finish the session:
        [writerInput markAsFinished];
        [videoWriter finishWriting];
        
        callBack();
    }];
    
    NSLog(@"outside for loop");
    
}

下面这个方法是拿到 一个 buffer 的 , 通过它去拼接视频.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image size:(CGSize)size
{
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
    CVPixelBufferRef pxbuffer = NULL;
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
    
    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
    
    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(pxdata != NULL);
    
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4 * size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
    NSParameterAssert(context);
    
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
    
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    return pxbuffer;
}

这里会有一个内存飙升的问题,所以要注意 release.

GPUImage录像的一些备忘

From: http://www.hudongdong.com/ios/530.html

GPUImage在加滤镜、图像处理上面集成的很完善,现在在使用的时候碰到的一些问题做下总结备忘,这些都是弄了很长时间才找到原因,如果碰到相似的可以节省下时间。当然简单的使用也可以看这个文章《IOS使用GPUImage滤镜初级试水

1、摄像头左右相反

使用GPU录像的时候,如果前置摄像头在左右是相反的,那么需要设置下镜像设置了

//如果是调用前置摄像头就发现成的像是左右相反了,就看下是不是开启了镜像
self.videoCamera.horizontallyMirrorFrontFacingCamera = YES;
self.videoCamera.horizontallyMirrorRearFacingCamera = NO;

2、开始录像的时候会图像会高亮下

如果录像的额时候,图像高亮下,可以在初始化GPUImageVideoCamera的时候,加上

[m_videoCamera addAudioInputsAndOutputs];

这个是为camera的增加的声音输入,但是这个如果是在一个界面的时候,频繁的增加有可能会造成录制的画面卡顿,所以可以在界面初始化Viewdidload的时候,初始化camera的时候加上这个代码。

3、第一帧偶尔黑屏

如果第一帧黑屏,应该是在开始录制等时间的问题,可以把要使用的视频第一帧裁减掉,裁剪视频的方案晚点会写成一个总结。

4、暂停/重录视频

如果仅仅是暂停录制,不暂停摄像头画面

//录制暂停
[m_movieWriter setPaused:true];
//录制继续
[m_movieWriter setPaused:false];

如果是暂停录制,并且也要暂停摄像头画面

//录制暂停
[m_movieWriter setPaused:true];
//摄像头暂停
[m_videoCamera pauseCameraCapture];
//录制继续
[m_movieWriter setPaused:false];
//摄像头继续
[m_videoCamera resumeCameraCapture];

5、暂停又重录会黑屏或者最后几秒有画面没声音

可以进入GPUImageMovieWriter.m中,在

- (void)processAudioBuffer:(CMSampleBufferRef)audioBuffer;

把里面的一段函数先注销掉

if (offsetTime.value > 0) {
     CFRelease(audioBuffer);
     audioBuffer = [self adjustTime:audioBuffer by:offsetTime];
     CFRetain(audioBuffer);
}

6、视频录制的代码

6.1、设置变量

//摄像头
    GPUImageVideoCamera *m_videoCamera;
    GPUImageFilter *m_customFilter;
    GPUImageView *m_filteredVideoView;
    //录制
    GPUImageMovieWriter *m_movieWriter;
    NSMutableDictionary * videoSettings; //字面意思.视频设置
    NSDictionary * audioSettings; //声音设置

6.2、初始化摄像头

m_videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset1280x720 cameraPosition:AVCaptureDevicePositionBack];
//    [m_videoCamera setFrameRate:60];
    //前置摄像头
    m_videoCamera.horizontallyMirrorFrontFacingCamera = YES;
    m_videoCamera.horizontallyMirrorRearFacingCamera = NO;
    m_videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    [m_videoCamera addAudioInputsAndOutputs];
//    m_customFilter = [[AmomoFilter alloc] init];
    m_customFilter = [[GPUImageFilter alloc] init];

    m_filteredVideoView = [[GPUImageView alloc] initWithFrame:CGRectMake(0.0,0.0,JLXScreenSize.width,JLXScreenSize.height)];
    // Add the view somewhere so it's visible
    [self.view addSubview:m_filteredVideoView];

    //添加滤镜
    [m_videoCamera addTarget:m_customFilter];
    [m_customFilter addTarget:m_filteredVideoView];
    [m_videoCamera startCameraCapture];

6.3、初始化movieWriter

-(void)initcustomWriter
{
    oldVideoPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie4.mov"];
    unlink([oldVideoPath UTF8String]); // 如果已经存在文件,AVAssetWriter会有异常,删除旧文件
    NSURL *movieURL = [JLXNetTools conVertToURL:oldVideoPath];

    //视频设置
    videoSettings = [[NSMutableDictionary alloc] init];
    [videoSettings setObject:AVVideoCodecH264 forKey:AVVideoCodecKey];
    [videoSettings setObject:[NSNumber numberWithInteger:720] forKey:AVVideoWidthKey]; //视频的宽度,这里最好是定义imageCamera时候的宽度
    [videoSettings setObject:[NSNumber numberWithInteger:1280] forKey:AVVideoHeightKey]; //视频的高度.同上

    //音频设置
    AudioChannelLayout channelLayout;
    memset(&channelLayout, 0, sizeof(AudioChannelLayout));
    channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
    audioSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                     [ NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
                     [ NSNumber numberWithInt: 2 ], AVNumberOfChannelsKey,
                     [ NSNumber numberWithFloat: 16000.0 ], AVSampleRateKey,
                     [ NSData dataWithBytes:&channelLayout length: sizeof( AudioChannelLayout ) ], AVChannelLayoutKey,
                     [ NSNumber numberWithInt: 32000 ], AVEncoderBitRateKey,
                     nil];

    m_movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(720, 1280) fileType:AVFileTypeQuickTimeMovie outputSettings:videoSettings];
//    [m_movieWriter setHasAudioTrack:YES audioSettings:audioSettings];
    [m_movieWriter setHasAudioTrack:true];
    m_videoCamera.audioEncodingTarget = m_movieWriter; //设置声音
//    m_movieWriter.assetWriter.movieFragmentInterval = kCMTimeInvalid; //Couldn't write a frame
    m_movieWriter.encodingLiveVideo = YES;
    [m_customFilter addTarget:m_movieWriter];
}

6.4、开始录制

[m_movieWriter startRecording];

6.5、结束录制

[m_movieWriter finishRecordingWithCompletionHandler:^{
   }];
[m_customFilter removeTarget:m_movieWriter];
m_videoCamera.audioEncodingTarget = nil;

6.6、暂停/重录视频

如果仅仅是暂停录制,不暂停摄像头画面

//录制暂停
[m_movieWriter setPaused:true];
//录制继续
[m_movieWriter setPaused:false];

如果是暂停录制,并且也要暂停摄像头画面

//录制暂停
[m_movieWriter setPaused:true];
//摄像头暂停
[m_videoCamera pauseCameraCapture];
//录制继续
[m_movieWriter setPaused:false];
//摄像头继续
[m_videoCamera resumeCameraCapture];

6.7、取消录制

[m_movieWriter cancelRecording];

七、视频滤镜

7.1、一个滤镜

    //添加滤镜
m_customFilter = [[GPUImageFilter alloc] init];
[m_videoCamera addTarget:m_customFilter];
[m_customFilter addTarget:m_filteredVideoView];
[m_customFilter addTarget:m_movieWriter];

7.2、多个滤镜

多个滤镜需要使用滤镜组

//漫画
    GPUImageSketchFilter *disFilter = [[GPUImageSketchFilter alloc] init];
    //褐色
    GPUImageSepiaFilter* sepiaFilter = [[GPUImageSepiaFilter alloc] init];

    filterGroup = [[GPUImageFilterGroup alloc] init];
    [filterGroup addFilter:disFilter];
    [filterGroup addFilter:sepiaFilter];

    //先后顺序
    [disFilter addTarget:sepiaFilter];

    //开始的滤镜
    [filterGroup setInitialFilters:[NSArray arrayWithObject:disFilter]];
    [filterGroup setTerminalFilter:sepiaFilter];

    [filterGroup addTarget:m_filteredVideoView];
    [m_videoCamera addTarget:filterGroup];

    [filterGroup addTarget:m_movieWriter];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
GPUImage是Brad Larson在github托管的一个开源项目,项目实现了图片滤镜、摄像头实时滤镜,该项目的优点不但在于滤镜很多,而且处理效果是基于GPU的,比使用CPU性能更高。
下载地址是:https://github.com/BradLarson/GPUImage
已有的一些filter介绍:
#import "GPUImageBrightnessFilter.h" //亮度
#import "GPUImageExposureFilter.h" //曝光
#import "GPUImageContrastFilter.h" //对比度
#import "GPUImageSaturationFilter.h" //饱和度
#import "GPUImageGammaFilter.h" //伽马线
#import "GPUImageColorInvertFilter.h" //反色
#import "GPUImageSepiaFilter.h" //褐色(怀旧)
#import "GPUImageLevelsFilter.h" //色阶
#import "GPUImageGrayscaleFilter.h" //灰度
#import "GPUImageHistogramFilter.h" //色彩直方图,显示在图片上
#import "GPUImageHistogramGenerator.h" //色彩直方图
#import "GPUImageRGBFilter.h" //RGB
#import "GPUImageToneCurveFilter.h" //色调曲线
#import "GPUImageMonochromeFilter.h" //单色
#import "GPUImageOpacityFilter.h" //不透明度
#import "GPUImageHighlightShadowFilter.h" //提亮阴影
#import "GPUImageFalseColorFilter.h" //色彩替换(替换亮部和暗部色彩)
#import "GPUImageHueFilter.h" //色度
#import "GPUImageChromaKeyFilter.h" //色度键
#import "GPUImageWhiteBalanceFilter.h" //白平横
#import "GPUImageAverageColor.h" //像素平均色值
#import "GPUImageSolidColorGenerator.h" //纯色
#import "GPUImageLuminosity.h" //亮度平均
#import "GPUImageAverageLuminanceThresholdFilter.h" //像素色值亮度平均,图像黑白(有类似漫画效果)
#import "GPUImageLookupFilter.h" //lookup 色彩调整
#import "GPUImageAmatorkaFilter.h" //Amatorka lookup
#import "GPUImageMissEtikateFilter.h" //MissEtikate lookup
#import "GPUImageSoftEleganceFilter.h" //SoftElegance lookup
#pragma mark - 图像处理 Handle Image
#import "GPUImageCrosshairGenerator.h" //十字
#import "GPUImageLineGenerator.h" //线条
#import "GPUImageTransformFilter.h" //形状变化
#import "GPUImageCropFilter.h" //剪裁
#import "GPUImageSharpenFilter.h" //锐化
#import "GPUImageUnsharpMaskFilter.h" //反遮罩锐化
#import "GPUImageFastBlurFilter.h" //模糊
#import "GPUImageGaussianBlurFilter.h" //高斯模糊
#import "GPUImageGaussianSelectiveBlurFilter.h" //高斯模糊,选择部分清晰
#import "GPUImageBoxBlurFilter.h" //盒状模糊
#import "GPUImageTiltShiftFilter.h" //条纹模糊,中间清晰,上下两端模糊
#import "GPUImageMedianFilter.h" //中间值,有种稍微模糊边缘的效果
#import "GPUImageBilateralFilter.h" //双边模糊
#import "GPUImageErosionFilter.h" //侵蚀边缘模糊,变黑白
#import "GPUImageRGBErosionFilter.h" //RGB侵蚀边缘模糊,有色彩
#import "GPUImageDilationFilter.h" //扩展边缘模糊,变黑白
#import "GPUImageRGBDilationFilter.h" //RGB扩展边缘模糊,有色彩
#import "GPUImageOpeningFilter.h" //黑白色调模糊
#import "GPUImageRGBOpeningFilter.h" //彩色模糊
#import "GPUImageClosingFilter.h" //黑白色调模糊,暗色会被提亮
#import "GPUImageRGBClosingFilter.h" //彩色模糊,暗色会被提亮
#import "GPUImageLanczosResamplingFilter.h" //Lanczos重取样,模糊效果
#import "GPUImageNonMaximumSuppressionFilter.h" //非最大抑制,只显示亮度最高的像素,其他为黑
#import "GPUImageThresholdedNonMaximumSuppressionFilter.h" //与上相比,像素丢失更多
#import "GPUImageSobelEdgeDetectionFilter.h" //Sobel边缘检测算法(白边,黑内容,有点漫画的反色效果)
#import "GPUImageCannyEdgeDetectionFilter.h" //Canny边缘检测算法(比上更强烈的黑白对比度)
#import "GPUImageThresholdEdgeDetectionFilter.h" //阈值边缘检测(效果与上差别不大)
#import "GPUImagePrewittEdgeDetectionFilter.h" //普瑞维特(Prewitt)边缘检测(效果与Sobel差不多,貌似更平滑)
#import "GPUImageXYDerivativeFilter.h" //XYDerivative边缘检测,画面以蓝色为主,绿色为边缘,带彩色
#import "GPUImageHarrisCornerDetectionFilter.h" //Harris角点检测,会有绿色小十字显示在图片角点处
#import "GPUImageNobleCornerDetectionFilter.h" //Noble角点检测,检测点更多
#import "GPUImageShiTomasiFeatureDetectionFilter.h" //ShiTomasi角点检测,与上差别不大
#import "GPUImageMotionDetector.h" //动作检测
#import "GPUImageHoughTransformLineDetector.h" //线条检测
#import "GPUImageParallelCoordinateLineTransformFilter.h" //平行线检测
#import "GPUImageLocalBinaryPatternFilter.h" //图像黑白化,并有大量噪点
#import "GPUImageLowPassFilter.h" //用于图像加亮
#import "GPUImageHighPassFilter.h" //图像低于某值时显示为黑
#pragma mark - 视觉效果 Visual Effect
#import "GPUImageSketchFilter.h" //素描
#import "GPUImageThresholdSketchFilter.h" //阀值素描,形成有噪点的素描
#import "GPUImageToonFilter.h" //卡通效果(黑色粗线描边)
#import "GPUImageSmoothToonFilter.h" //相比上面的效果更细腻,上面是粗旷的画风
#import "GPUImageKuwaharaFilter.h" //桑原(Kuwahara)滤波,水粉画的模糊效果;处理时间比较长,慎用
#import "GPUImageMosaicFilter.h" //黑白马赛克
#import "GPUImagePixellateFilter.h" //像素化
#import "GPUImagePolarPixellateFilter.h" //同心圆像素化
#import "GPUImageCrosshatchFilter.h" //交叉线阴影,形成黑白网状画面
#import "GPUImageColorPackingFilter.h" //色彩丢失,模糊(类似监控摄像效果)
#import "GPUImageVignetteFilter.h" //晕影,形成黑色圆形边缘,突出中间图像的效果
#import "GPUImageSwirlFilter.h" //漩涡,中间形成卷曲的画面
#import "GPUImageBulgeDistortionFilter.h" //凸起失真,鱼眼效果
#import "GPUImagePinchDistortionFilter.h" //收缩失真,凹面镜
#import "GPUImageStretchDistortionFilter.h" //伸展失真,哈哈镜
#import "GPUImageGlassSphereFilter.h" //水晶球效果
#import "GPUImageSphereRefractionFilter.h" //球形折射,图形倒立
#import "GPUImagePosterizeFilter.h" //色调分离,形成噪点效果
#import "GPUImageCGAColorspaceFilter.h" //CGA色彩滤镜,形成黑、浅蓝、紫色块的画面
#import "GPUImagePerlinNoiseFilter.h" //柏林噪点,花边噪点
#import "GPUImage3x3ConvolutionFilter.h" //3x3卷积,高亮大色块变黑,加亮边缘、线条等
#import "GPUImageEmbossFilter.h" //浮雕效果,带有点3d的感觉
#import "GPUImagePolkaDotFilter.h" //像素圆点花样
#import "GPUImageHalftoneFilter.h" //点染,图像黑白化,由黑点构成原图的大致图形
#pragma mark - 混合模式 Blend
#import "GPUImageMultiplyBlendFilter.h" //通常用于创建阴影和深度效果
#import "GPUImageNormalBlendFilter.h" //正常
#import "GPUImageAlphaBlendFilter.h" //透明混合,通常用于在背景上应用前景的透明度
#import "GPUImageDissolveBlendFilter.h" //溶解
#import "GPUImageOverlayBlendFilter.h" //叠加,通常用于创建阴影效果
#import "GPUImageDarkenBlendFilter.h" //加深混合,通常用于重叠类型
#import "GPUImageLightenBlendFilter.h" //减淡混合,通常用于重叠类型
#import "GPUImageSourceOverBlendFilter.h" //源混合
#import "GPUImageColorBurnBlendFilter.h" //色彩加深混合
#import "GPUImageColorDodgeBlendFilter.h" //色彩减淡混合
#import "GPUImageScreenBlendFilter.h" //屏幕包裹,通常用于创建亮点和镜头眩光
#import "GPUImageExclusionBlendFilter.h" //排除混合
#import "GPUImageDifferenceBlendFilter.h" //差异混合,通常用于创建更多变动的颜色
#import "GPUImageSubtractBlendFilter.h" //差值混合,通常用于创建两个图像之间的动画变暗模糊效果
#import "GPUImageHardLightBlendFilter.h" //强光混合,通常用于创建阴影效果
#import "GPUImageSoftLightBlendFilter.h" //柔光混合
#import "GPUImageChromaKeyBlendFilter.h" //色度键混合
#import "GPUImageMaskFilter.h" //遮罩混合
#import "GPUImageHazeFilter.h" //朦胧加暗
#import "GPUImageLuminanceThresholdFilter.h" //亮度阈
#import "GPUImageAdaptiveThresholdFilter.h" //自适应阈值
#import "GPUImageAddBlendFilter.h" //通常用于创建两个图像之间的动画变亮模糊效果
#import "GPUImageDivideBlendFilter.h" //通常用于创建两个图像之间的动画变暗模糊效果

记录Setting的跳转URL Scheme

// 更新ios8 后可以直接跳转到应用隐私设置

1
UIApplication.shared.openURL(URL(string:UIApplicationOpenSettingsURLString)!)

跳转方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
打开系统设置 等open
@param scheme <#scheme description#>
*/
-(void)openSysScheme:(NSString*)scheme{
UIApplication *application = [UIApplication sharedApplication];
NSURL *URL = [NSURL URLWithString:scheme];
if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) {
[application openURL:URL options:@{}
completionHandler:^(BOOL success) {
NSLog(@"Open %@: %d",scheme,success);
}];
} else {
BOOL success = [application openURL:URL];
NSLog(@"Open %@: %d",scheme,success);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func openSysSeting(type:openSysSetingType){
switch type {
case .photoPrivacy:// 相册的隐私设置
if #available(iOS 10.0, *) {
// UIApplication.shared.open(URL(string:"App-prefs:root=Photos")!)
UIApplication.shared.open(URL(string:"App-prefs:root=Privacy&path=PHOTOS")!, options: [:], completionHandler: { (bool) in
})
} else {
UIApplication.shared.openURL(URL(string:"prefs:root=Privacy&path=PHOTOS")!)
// Fallback on earlier versions
}
default:
break
}
}

From: http://noahzy.com/ji-lu-settingde-tiao-zhuan-url/

记录下各个页面的跳转URL Scheme,方便以后查询

IOS 10 后
"App-prefs:root=Privacy&path=CAMERA"

WIFI prefs:root=WIFI 个人热点 prefs:root=INTERNET_TETHERING 蜂窝设置 prefs:root=MOBILE_DATA_SETTINGS_ID 隐私 prefs:root=Privacy 隐私-相机 prefs:root=Privacy&path=CAMERA 隐私-相册 prefs:root=Privacy&path=PHOTOS 隐私-定位服务 prefs:root=LOCATION_SERVICES 通用 prefs:root=General&path VPN prefs:root=VPN 关于 prefs:root=General&path=About

辅助功能 prefs:root=General&path=ACCESSIBILITY 飞行模式 prefs:root=AIRPLANE_MODE 自动锁屏时间 prefs:root=General&path=AUTOLOCK 用量 prefs:root=General&path=USAGE 亮度 prefs:root=Brightness 蓝牙 prefs:root=General&path=Bluetooth 日期和时间 prefs:root=General&path=DATE_AND_TIME Facetime设置 prefs:root=FACETIME 键盘设置 prefs:root=General&path=Keyboard Icloud prefs:root=CASTLE 备份 prefs:root=CASTLE&path=STORAGE_AND_BACKUP 语言与地区设置 prefs:root=General&path=INTERNATIONAL 账户设置 prefs:root=ACCOUNT_SETTINGS 音乐 prefs:root=MUSIC EQ均衡器 prefs:root=MUSIC&path=EQ 设置 prefs:root= 备忘录 prefs:root=NOTES 通知 prefs:root=NOTIFICATIONS_ID 电话设置 prefs:root=Phone 照片与相机设置 prefs:root=Photos 还原 prefs:root=General&path=Reset 铃声设置 prefs:root=Sounds&path=Ringtone Safari prefs:root=Safari 声音 prefs:root=Sounds 系统更新 prefs:root=General&path=SOFTWARE_UPDATE_LINK STORE 设置 prefs:root=STORE 视频设置 prefs:root=VIDEO 壁纸设置 prefs:root=Wallpaper


2018-6-20 更新
苹果不让用这个私有方式了

From: https://www.jianshu.com/p/a94cd8103bba

例如跳转Wi-Fi,之前是使用prefs:root=WIFI来进行跳转

eg:

//iOS10
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"prefs:root=WIFI"] options:@{} completionHandler:nil];
//
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"prefs:root=WIFI"]];

这样在上架的时候会悲剧

中间进行一个转码,绕过苹果的代码扫描,亲测能过审核.

//将上面的跳转字符串转成字符,在进行拼接就好了
NSData *encryptString = [[NSData alloc] initWithBytes:(unsigned char []){0x70,0x72,0x65,0x66,0x73,0x3a,0x72,0x6f,0x6f,0x74,0x3d,0x4e,0x4f,0x54,0x49,0x46,0x49,0x43,0x41,0x54,0x49,0x4f,0x4e,0x53,0x5f,0x49,0x44} length:27];

NSString *string = [[NSString alloc] initWithData:encryptString encoding:NSUTF8StringEncoding];

[[UIApplication sharedApplication] openURL:[NSURL URLWithString:string] options:@{} completionHandler:nil];

swift

1
2
let dataString = Data.init(bytes: [0x70,0x72,0x65,0x66,0x73,0x3a,0x72,0x6f,0x6f,0x74,0x3d,0x50,0x72,0x69,0x76,0x61,0x63,0x79,0x26,0x70,0x61,0x74,0x68,0x3d,0x50,0x48,0x4f,0x54,0x4f,0x53])
let string = String.init(data: dataString, encoding: String.Encoding.utf8)

给一个转换的网站

http://www.ab126.com/goju/1711.html

一般ios 8 都用新的方式了, ios7 就这个转码吧

iOS中为直播APP集成美颜功能

iOS中为直播APP集成美颜功能 获取GPUImage 处理后的buffer

From: http://www.jianshu.com/p/dde412cab8db

最近需要给直播项目中添加美颜的功能,调研了很多SDK和开源代码(视决,涂图,七牛,金山云,videoCore等),综合成本/效果/对项目侵入性,最后决定使用一款基于GPUImage实现的 BeautifyFaceDemo美颜滤镜。

关于滤镜代码和实现思路可以到BeautifyFace Github和作者琨君简书中查看。

集成GPUImageBeautifyFilter和GPUImage Framework

首先需要集成好GPUImage,通过观察目前iOS平台,90%以上美颜方案都是基于这个框架来做的。
原来项目中的AVCaptureDevice需要替换成GPUImageVideoCamera,删除诸如AVCaptureSession/AVCaptureDeviceInput/AVCaptureVideoDataOutput这种GPUImage实现了的部分。修改一些生命周期,摄像头切换,横竖屏旋转等相关逻辑,保证前后行为统一。

声明需要的属性

@property (nonatomic, strong) GPUImageVideoCamera *videoCamera;  
//屏幕上显示的View
@property (nonatomic, strong) GPUImageView *filterView;
//BeautifyFace美颜滤镜
@property (nonatomic, strong) GPUImageBeautifyFilter *beautifyFilter;

然后初始化

self.sessionPreset = AVCaptureSessionPreset1280x720;
self.videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:self.sessionPreset cameraPosition:AVCaptureDevicePositionBack];

self.filterView = [[GPUImageView alloc] init];
[self.view insertSubview:self.filterView atIndex:1]; //省略frame的相关设置

//这里我在GPUImageBeautifyFilter中增加个了初始化方法用来设置美颜程度intensity
self.beautifyFilter = [[GPUImageBeautifyFilter alloc] initWithIntensity:0.6];

为filterView增加美颜滤镜

[self.videoCamera addTarget:self.beautifyFilter];
[self.beautifyFilter addTarget:self.filterView];

然后调用startCameraCapture方法就可以看到效果了

[self.videoCamera startCameraCapture];

到这里,仅仅是屏幕显示的内容带有滤镜效果,而作为直播应用,还需要输出带有美颜效果的视频流

输出带有美颜效果的视频流

刚开始集成的时候碰见一个坑,原本的逻辑是实现AVCaptureVideoDataOutputSampleBufferDelegate方法来获得原始帧

- (void) captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;

而GPUImageVideoCamera也实现了一个类似的代理:

@protocol GPUImageVideoCameraDelegate <NSObject>
@optional
- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer;
@end

而替换之后发现输出的流依旧是未经美颜的图像,看了实现后发现果不其然,GPUImageVideoCameraDelegate还是通过AVCaptureVideoDataOutputSampleBufferDelegate直接返回的数据,所以想输出带有滤镜的流这里就得借助GPUImageRawDataOutput了

CGSize outputSize = {720, 1280};
GPUImageRawDataOutput *rawDataOutput = [[GPUImageRawDataOutput alloc] initWithImageSize:CGSizeMake(outputSize.width, outputSize.height) resultsInBGRAFormat:YES];
[self.beautifyFilter addTarget:rawDataOutput];

这个GPUImageRawDataOutput其实就是beautifyFilter的输出工具,可在setNewFrameAvailableBlock方法的block中获得带有滤镜效果的数据

__weak GPUImageRawDataOutput *weakOutput = rawDataOutput;
__weak typeof(self) weakSelf = self;

[rawDataOutput setNewFrameAvailableBlock:^{
    __strong GPUImageRawDataOutput *strongOutput = weakOutput;
    [strongOutput lockFramebufferForReading];

    // 这里就可以获取到添加滤镜的数据了
    GLubyte *outputBytes = [strongOutput rawBytesForImage];
    NSInteger bytesPerRow = [strongOutput bytesPerRowInOutput];
    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferCreateWithBytes(kCFAllocatorDefault, outputSize.width, outputSize.height, kCVPixelFormatType_32BGRA, outputBytes, bytesPerRow, nil, nil, nil, &pixelBuffer);

    // 之后可以利用VideoToolBox进行硬编码再结合rtmp协议传输视频流了
    [weakSelf encodeWithCVPixelBufferRef:pixelBuffer];

    [strongOutput unlockFramebufferAfterReading];
    CFRelease(pixelBuffer);

}];

目前依旧存在的问题

经过和其他产品对比,GPUImageBeautifyFilter磨皮效果和花椒最为类似。这里采用双边滤波, 花椒应该用了高斯模糊实现。同印客对比,美白效果一般。

还存在些关于性能的问题:

1 调用setNewFrameAvailableBlock后很多机型只能跑到不多不少15fps
2 在6s这代机型上温度很高,帧率可到30fps但不稳定

Update(8-13)

  1. 关于性能问题,最近把项目中集成的美颜滤镜(BeautifyFace)里用到的 GPUImageCannyEdgeDetectionFilter 替换为 GPUImageSobelEdgeDetectionFilter 会有很大改善,而且效果几乎一致,6s经过长时间测试没有再次出现高温警告了。(替换也十分简单,直接改俩处类名/变量名就可以了)

  2. 分享一个BUG,最近发现当开启美颜的时候,关闭直播内存竟然没有释放。分析得出GPUImageRawDataOutput的setNewFrameAvailableBlock方法的block参数仍然保持着self,解决思路就是将GPUImageRawDataOutput移除。

先附上之前的相关release代码:

[self.videoCamera stopCameraCapture];
[self.videoCamera removeInputsAndOutputs];
[self.videoCamera removeAllTargets];

开始以为camera调用removeAllTargets会把camera上面的filter,以及filter的output一同释放,但实际camera并不会’帮忙’移除filter的target,所以需要添加:

[self.beautifyFilter removeAllTargets]; //修复开启美颜内存无法释放的问题

关闭美颜output是直接加在camera上,camera直接removeAllTargets就可以;
开启美颜output加在filter上,camera和filter都需要removeAllTargets。


oc 版本的转图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/ CIContext 的 - render:toBitmap:rowBytes:bounds:format:colorSpace 据说这个会因为系统问题ios9.0 ,产生内存泄漏
// AVFoundation 捕捉视频帧,很多时候都需要把某一帧转换成 image
+ (CGImageRef)imageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef
{
@autoreleasepool { // 可以加一个自动内存管理
// 为媒体数据设置一个CMSampleBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBufferRef);
// 锁定 pixel buffer 的基地址
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// 得到 pixel buffer 的基地址
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
// 得到 pixel buffer 的行字节数
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// 得到 pixel buffer 的宽和高
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 创建一个依赖于设备的 RGB 颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();//CGColorSpaceCreateDeviceGray
/// 据说GPUImage 只能只用这个
//CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
// 用抽样缓存的数据创建一个位图格式的图形上下文(graphic context)对象
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//根据这个位图 context 中的像素创建一个 Quartz image 对象
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
// 解锁 pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
// 释放 context 和颜色空间
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// 用 Quzetz image 创建一个 UIImage 对象
// UIImage *image = [UIImage imageWithCGImage:quartzImage];
// 释放 Quartz image 对象
// CGImageRelease(quartzImage);
return quartzImage;
}
}

swift 版本获取buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
///摄像头
var videoCamera:GPUImageVideoCamera!
var movieWriter:GPUImageMovieWriter!
var filterGroup:GPUImageFilterGroup!
var gpuImageView:GPUImageView!
var recSize = CGSize(width: 640, height: 480)
/// 创建测试GPUIamge 放假录
func createGPUImage(){
filterGroup = GPUImageFilterGroup()
videoCamera = GPUImageVideoCamera(sessionPreset: AVCaptureSessionPreset640x480, cameraPosition: AVCaptureDevicePosition.back)
videoCamera.outputImageOrientation = .portrait
videoCamera.addAudioInputsAndOutputs() // 开启声音捕获
videoCamera.horizontallyMirrorRearFacingCamera = false
videoCamera.horizontallyMirrorFrontFacingCamera = false// 镜像策略
let filter = GPUImageSepiaFilter()
videoCamera.addTarget(filterGroup)
filterGroup.addFilter(filter)
videoCamera.delegate = self
// videoCamera.outputTextureOptions =
gpuImageView = GPUImageView(frame: self.view.bounds)
self.view.addSubview(gpuImageView)
// 必须设置开始和结尾不然白屏
filterGroup.initialFilters = [filter]
filterGroup.terminalFilter = filter
filterGroup.addTarget(gpuImageView)
let dataoutput = GPUImageRawDataOutput(imageSize: recSize, resultsInBGRAFormat: true)
filterGroup.addTarget(dataoutput)
weak var weakDataoutput = dataoutput
weak var weakSelf = self
dataoutput?.newFrameAvailableBlock = {
if let weakDataoutput = weakDataoutput{
weakDataoutput.lockFramebufferForReading()
let outbytes = weakDataoutput.rawBytesForImage
let bytesPerRow = weakDataoutput.bytesPerRowInOutput()
var pixelBuffer:CVPixelBuffer? = nil
CVPixelBufferCreateWithBytes(kCFAllocatorDefault, Int(weakSelf!.recSize.width), Int(weakSelf!.recSize.height), kCVPixelFormatType_32BGRA, outbytes!, Int(bytesPerRow), nil, nil, nil, &pixelBuffer)
// 如果直播就可以发送这个流的 pixelBuffer
weakDataoutput.unlockFramebufferAfterReading()
//swift 不需要CFRelease(pixelBuffer)
}
}
videoCamera.startCapture()
// DispatchQueue.main.async {
// self.startREC()
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+5) {
// self.stopREC()
// }
// }
}
func startREC(){
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]+"/test.mov"
unlink(path.cString(using: String.Encoding.utf8))
movieWriter = GPUImageMovieWriter(movieURL: URL(fileURLWithPath: path), size: recSize)
movieWriter.encodingLiveVideo = true
videoCamera.audioEncodingTarget = movieWriter
filterGroup.addTarget(movieWriter)
movieWriter.startRecording()
}
func stopREC(){
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]+"/test.mov"
filterGroup.removeAllTargets()
videoCamera.audioEncodingTarget = nil
movieWriter.finishRecording()
UISaveVideoAtPathToSavedPhotosAlbum(path, self, nil, nil)
}

swift 3 的转码 CMSampleBuffer -> UIImage

    func willOutputSampleBuffer(_ sampleBuffer: CMSampleBuffer!) {
                                               // CMSampleBufferRef
        if  sampleBuffer != nil{
//        let cimage = BuilderVideo.image(fromSampleBufferRef: sampleBuffer)
            let myPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
            let myCIimage         = CIImage(cvPixelBuffer: myPixelBuffer!)
           let videoImage        = UIImage(ciImage: myCIimage)
            DispatchQueue.main.async {
                self.testImageView.image = videoImage
            }

        }
//        CGImageRelease


    }

}


    //参考http://stackoverflow.com/questions/41623186/cmsamplebuffer-from-avcapturevideodataoutput-unexpectedly-found-nil

iOS GPUImage源码解读(一)

iOS GPUImage源码解读(一)
From: https://syxblog.com/2017/04/1884.html

GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,内置大量滤镜,架构灵活,可以在其基础上很轻松地实现各种图像处理功能。本文主要向大家分享一下项目的核心架构、源码解读及使用心得。

GPUImage有哪些特性

1.丰富的输入组件

摄像头、图片、视频、OpenGL纹理、二进制数据、UIElement(UIView, CALayer)

2.大量现成的内置滤镜(4大类)

1). 颜色类(亮度、色度、饱和度、对比度、曲线、白平衡…)

2). 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果…)

3). 颜色混合类(差异混合、alpha混合、遮罩混合…)

4). 效果类(像素化、素描效果、压花效果、球形玻璃效果…)

3.丰富的输出组件

UIView、视频文件、GPU纹理、二进制数据

4.灵活的滤镜链

滤镜效果之间可以相互串联、并联,调用管理相当灵活。

5.接口易用

滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用,并且内置了一个cache模块实现了framebuffer的复用。

6.线程管理

OpenGLContext不是多线程安全的,GPUImage创建了专门的contextQueue,所有的滤镜都会扔到统一的线程中处理。

7.轻松实现自定义滤镜效果

继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

基本用法

// 获取一张图片

效果如图:

iOS GPUImage源码解读(一)

整个框架的目录结构

iOS GPUImage源码解读(一)

核心架构

iOS GPUImage源码解读(一)

基本上每个滤镜都继承自GPUImageFilter;

而GPUImageFilter作为整套框架的核心;

接收一个GPUImageFrameBuffer输入;

调用GLProgram渲染处理;

输出一个GPUImageFrameBuffer;

把输出的GPUImageFrameBuffer传给通过targets属性关联的下级滤镜;

直到传递至最终的输出组件;

核心架构可以整体划分为三块:输入、滤镜处理、输出

接下来我们就深入源码,看看GPUImage是如何获取数据、传递数据、处理数据和输出数据的

获取数据

GPUImage提供了多种不同的输入组件,但是无论是哪种输入源,获取数据的本质都是把图像数据转换成OpenGL纹理。这里就以视频拍摄组件(GPUImageVideoCamera)为例,来讲讲GPUImage是如何把每帧采样数据传入到GPU的。

GPUImageVideoCamera里大部分代码都是对摄像头的调用管理,不了解的同学可以去学习一下AVFoundation(传送门)。摄像头拍摄过程中每一帧都会有一个数据回调,在GPUImageVideoCamera中对应的处理回调的方法为:

- (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

iOS的每一帧摄像头采样数据都会封装成CMSampleBufferRef;CMSampleBufferRef除了包含图像数据、还包含一些格式信息、图像宽高、时间戳等额外属性;

摄像头默认的采样格式为YUV420,关于YUV格式大家可以自行搜索学习一下(传送门):

iOS GPUImage源码解读(一)

YUV420按照数据的存储方式又可以细分成若干种格式,这里主要是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange和kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange两种;

两种格式都是planar类型的存储方式,y数据和uv数据分开放在两个plane中;这样的数据没法直接传给GPU去用,GPUImageVideoCamera把两个plane的数据分别取出:

-
 (void)processVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { // 
一大坨的代码用于获取采样数据的基本属性(宽、高、格式等等) ...... if ([GPUImageContext 
supportsFastTextureUpload] && captureAsYUV) { 
CVOpenGLESTextureRef luminanceTextureRef = ; CVOpenGLESTextureRef 
chrominanceTextureRef = ; if (CVPixelBufferGetPlaneCount(cameraFrame) 
> 0) // Check for YUV planar inputs to do RGB conversion {

注意CVOpenGLESTextureCacheCreateTextureFromImage中对于internalFormat的设置;

通常我们创建一般纹理的时候都会设成GL_RGBA,传入的图像数据也会是rgba格式的;

而这里y数据因为只包含一个通道,所以设成了GL_LUMINANCE(灰度图);

uv数据则包含2个通道,所以设成了GL_LUMINANCE_ALPHA(带alpha的灰度图);

另外uv纹理的宽高只设成了图像宽高的一半,这是因为yuv420中,每个相邻的2x2格子共用一份uv数据;

数据传到GPU纹理后,再通过一个颜色转换(yuv->rgb)的shader(shader是OpenGL可编程着色器,可以理解为GPU侧的代码,关于shader需要一些OpenGL编程基础(传送门)),绘制到目标纹理:

 // fullrange varying highp vec2 textureCoordinate; uniform sampler2D 
luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump 
mat3 colorConversionMatrix; void main { mediump vec3 yuv; lowp vec3 rgb;
 yuv.x = texture2D(luminanceTexture, textureCoordinate).r; yuv.yz = 
texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5); 
rgb = colorConversionMatrix * yuv; gl_FragColor = vec4(rgb, 1); }


 // videorange varying highp vec2 textureCoordinate; uniform sampler2D 
luminanceTexture; uniform sampler2D chrominanceTexture; uniform mediump 
mat3 colorConversionMatrix; void main { mediump vec3 yuv; lowp vec3 rgb;
 yuv.x = texture2D(luminanceTexture, textureCoordinate).r - 
(16.0/255.0); yuv.yz = texture2D(chrominanceTexture, 
textureCoordinate).ra - vec2(0.5, 0.5); rgb = colorConversionMatrix * 
yuv; gl_FragColor = vec4(rgb, 1); }

注意yuv420fullrange和yuv420videorange的数值范围是不同的,因此转换公式也不同,这里会有2个颜色转换shader,根据实际的采样格式选择正确的shader;

渲染输出到目标纹理后就得到一个转换成rgb格式的GPU纹理,完成了获取输入数据的工作;

传递数据

GPUImage的图像处理过程,被设计成了滤镜链的形式;输入组件、效果滤镜、输出组件串联在一起,每次推动渲染的时候,输入数据就会按顺序传递,经过处理,最终输出。

GPUImage设计了一个GPUImageInput协议,定义了GPUImageFilter之间传入数据的方法:

-
 (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer 
atIndex:(NSInteger)textureIndex { firstInputFramebuffer = 
newInputFramebuffer; [firstInputFramebuffer lock]; }

firstInputFramebuffer属性用来保存输入纹理;

GPUImageFilter作为单输入滤镜基类遵守了GPUImageInput协议,GPUImage还提供了GPUImageTwoInputFilter, GPUImageThreeInputFilter等多输入filter的基类。

这里还有一个很重要的入口方法用于推动数据流转:

-
 (void)newFrameReadyAtTime:(CMTime)frameTime 
atIndex:(NSInteger)textureIndex { ...... [self 
renderToTextureWithVertices:imageVertices textureCoordinates:[[self 
class] textureCoordinatesForRotation:inputRotation]]; [self 
informTargetsAboutNewFrameAtTime:frameTime]; }

每个滤镜都是由这个入口方法开始启动,这个方法包含2个调用

1). 首先调用render方法进行效果渲染

2). 调用informTargets方法将渲染结果推到下级滤镜

GPUImageFilter继承自GPUImageOutput,定义了输出数据,向后传递的方法:

- (void)notifyTargetsAboutNewOutputTexture;

但是这里比较奇怪的是滤镜链的传递实际并没有用notifyTargets方法,而是用了前面提到的informTargets方法:

-
 (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime { ...... // 
Get all targets the framebuffer so they can grab a lock on it for 
(id<GPUImageInput> currentTarget in targets) { if (currentTarget 
!= self.targetToIgnoreForUpdates) { NSInteger indexOfObject = [targets 
indexOfObject:currentTarget]; NSInteger textureIndex = 
[[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; [self
 setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; 
[currentTarget setInputSize:[self outputFrameSize] 
atIndex:textureIndex]; } } ...... // Trigger processing last, so that 
our unlock comes first in serial execution, avoiding the need for a 
callback for (id<GPUImageInput> currentTarget in targets) { if 
(currentTarget != self.targetToIgnoreForUpdates) { NSInteger 
indexOfObject = [targets indexOfObject:currentTarget]; NSInteger 
textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] 
integerValue]; [currentTarget newFrameReadyAtTime:frameTime 
atIndex:textureIndex]; } } }

GPUImageOutput定义了一个targets属性来保存下一级滤镜,这里可以注意到targets是个数组,因此滤镜链也支持并联结构。可以看到这个方法主要做了2件事情:

1). 对每个target调用setInputFramebuffer方法把自己的渲染结果传给下级滤镜作为输入

2). 对每个target调用newFrameReadyAtTime方法推动下级滤镜启动渲染

滤镜之间通过targets属性相互衔接串在一起,完成了数据传递工作。

iOS GPUImage源码解读(一)

处理数据

前面提到的renderToTextureWithVertices:方法便是每个滤镜必经的渲染入口。每个滤镜都可以设置自己的shader,重写该渲染方法,实现自己的效果:

-
 (void)renderToTextureWithVertices:(const GLfloat *)vertices 
textureCoordinates:(const GLfloat *)textureCoordinates { ...... 
[GPUImageContext setActiveShaderProgram:filterProgram]; 
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] 
fetchFramebufferForSize:[self sizeOfFBO] 
textureOptions:self.outputTextureOptions onlyTexture:NO]; 
[outputFramebuffer activateFramebuffer]; ...... [self 
setUniformsForProgramAtIndex:0]; glClearColor(backgroundColorRed, 
backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha); 
glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE2); 
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]); 
glUniform1i(filterInputTextureUniform, 2); 
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, 
vertices); glVertexAttribPointer(filterTextureCoordinateAttribute, 2, 
GL_FLOAT, 0, 0, textureCoordinates); glDrawArrays(GL_TRIANGLE_STRIP, 0, 
4); ...... }

上面这个是GPUImageFilter的默认方法,大致做了这么几件事情:

1). 向frameBufferCache申请一个outputFrameBuffer

2). 将申请得到的outputFrameBuffer激活并设为渲染对象

3). glClear清除画布

4). 设置输入纹理

5). 传入顶点

6). 传入纹理坐标

7). 调用绘制方法

再来看看GPUImageFilter使用的默认shader:

 // vertex shader attribute vec4 position; attribute vec4 
inputTextureCoordinate; varying vec2 textureCoordinate; void main { 
gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; }


 // fragment shader varying highp vec2 textureCoordinate; uniform 
sampler2D inputImageTexture; void main { gl_FragColor = 
texture2D(inputImageTexture, textureCoordinate); }

这个shader实际上啥也没做,VertexShader(顶点着色器)就是把传入的顶点坐标和纹理坐标原样做光栅化处理,FragmentShader(片段着色器)就是从纹理取出原始色值直接输出,最终效果就是把图片原样渲染到画面。

输出数据

比较常用的主要是GPUImageView和GPUImageMovieWriter。

GPUImageView继承自UIView,用于实时预览,用法非常简单

1). 创建GPUImageView

2). 串入滤镜链

3). 插到视图里去

UIView的contentMode、hidden、backgroundColor等属性都可以正常使用

里面比较关键的方法主要有这么2个:

// 申明自己的CALayer为CAEAGLLayer+ (Class)layerClass { return [CAEAGLLayer class]; }

-
 (void)createDisplayFramebuffer { [GPUImageContext 
useImageProcessingContext]; glGenFramebuffers(1, 
&displayFramebuffer); glBindFramebuffer(GL_FRAMEBUFFER, 
displayFramebuffer); glGenRenderbuffers(1, &displayRenderbuffer); 
glBindRenderbuffer(GL_RENDERBUFFER, displayRenderbuffer); 
[[[GPUImageContext sharedImageProcessingContext] context] 
renderbufferStorage:GL_RENDERBUFFER 
fromDrawable:(CAEAGLLayer*)self.layer]; GLint backingWidth, 
backingHeight; glGetRenderbufferParameteriv(GL_RENDERBUFFER, 
GL_RENDERBUFFER_WIDTH, &backingWidth); 
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, 
&backingHeight); ...... glFramebufferRenderbuffer(GL_FRAMEBUFFER, 
GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, displayRenderbuffer); ...... }

创建frameBuffer和renderBuffer时把renderBuffer和CALayer关联在一起;这是iOS内建的一种GPU渲染输出的联动方法;

这样newFrameReadyAtTime渲染过后画面就会输出到CALayer。

GPUImageMovieWriter主要用于将视频输出到磁盘;

里面大量的代码都是在设置和使用AVAssetWriter,不了解的同学还是得去看AVFoundation;

这里主要是重写了newFrameReadyAtTime:方法:

-
 (void)newFrameReadyAtTime:(CMTime)frameTime 
atIndex:(NSInteger)textureIndex { ...... GPUImageFramebuffer 
*inputFramebufferForBlock = firstInputFramebuffer; glFinish; 
runAsynchronouslyOnContextQueue(_movieWriterContext, ^{ ...... // Render
 the frame with swizzled colors, so that they can be uploaded quickly as
 BGRA frames [_movieWriterContext useAsCurrentContext]; [self 
renderAtInternalSizeUsingFramebuffer:inputFramebufferForBlock]; 
CVPixelBufferRef pixel_buffer = ; if ([GPUImageContext 
supportsFastTextureUpload]) { pixel_buffer = renderTarget; 
CVPixelBufferLockBaseAddress(pixel_buffer, 0); } else { CVReturn status =
 CVPixelBufferPoolCreatePixelBuffer (, [assetWriterPixelBufferInput 
pixelBufferPool], &pixel_buffer); if ((pixel_buffer == ) || (status 
!= kCVReturnSuccess)) { CVPixelBufferRelease(pixel_buffer); return; } 
else { CVPixelBufferLockBaseAddress(pixel_buffer, 0); GLubyte 
*pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
 glReadPixels(0, 0, videoSize.width, videoSize.height, GL_RGBA, 
GL_UNSIGNED_BYTE, pixelBufferData); } } ......

这里有几个地方值得注意:

1). 在取数据之前先调了一下glFinish,CPU和GPU之间是类似于client-server的关系,CPU侧调用OpenGL命令后并不是同步等待OpenGL完成渲染再继续执行的,而glFinish命令可以确保OpenGL把队列中的命令都渲染完再继续执行,这样可以保证后面取到的数据是正确的当次渲染结果。

2). 取数据时用了supportsFastTextureUpload判断,这是个从iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射(映射的创建可以参看获取数据中的

CVOpenGLESTextureCacheCreateTextureFromImage),通过这个映射可以直接拿到CVPixelBufferRef而不需要再用glReadPixel来读取数据,这样性能更好。

最后归纳一下本文涉及到的知识点

  1. AVFoundation

摄像头调用、输出视频都会用到AVFoundation

  1. YUV420

视频采集的数据格式

  1. OpenGL shader

GPU的可编程着色器

  1. CAEAGLLayer

iOS内建的GPU到屏幕的联动方法

  1. fastTextureUpload

iOS5开始支持的一种CVOpenGLESTextureCacheRef和CVImageBufferRef的映射

实现微信小视频iOS代码

From: http://www.pclic.com/ios/108553.html

前段时间项目要求需要在聊天模块中加入类似微信的小视频功能,这边博客主要是为了总结遇到的问题和解决方法,希望能够对有同样需求的朋友有所帮助。

这里先罗列遇到的主要问题:  
1.视频剪裁 微信的小视频只是取了摄像头获取的一部分画面
2.滚动预览的卡顿问题 AVPlayer播放视频在滚动中会出现很卡的问题

接下来让我们一步步来实现。
Part 1 实现视频录制
1.录制类WKMovieRecorder实现
创建一个录制类WKMovieRecorder,负责视频录制。

@interface WKMovieRecorder : NSObject

+ (WKMovieRecorder*) sharedRecorder; 
 - (instancetype)initWithMaxDuration:(NSTimeInterval)duration;
 @end 

定义回调block

/**
 * 录制结束
 *
 * @param info   回调信息
 * @param isCancle YES:取消 NO:正常结束
 */
typedef void(^FinishRecordingBlock)(NSDictionary *info, WKRecorderFinishedReason finishReason);
/**
 * 焦点改变
 */
typedef void(^FocusAreaDidChanged)();
/**
 * 权限验证
 *
 * @param success 是否成功
 */
typedef void(^AuthorizationResult)(BOOL success);

@interface WKMovieRecorder : NSObject
//回调
@property (nonatomic, copy) FinishRecordingBlock finishBlock;//录制结束回调
@property (nonatomic, copy) FocusAreaDidChanged focusAreaDidChangedBlock;
@property (nonatomic, copy) AuthorizationResult authorizationResultBlock;
@end

定义一个cropSize用于视频裁剪
@property (nonatomic, assign) CGSize cropSize;

接下来就是capture的实现了,这里代码有点长,懒得看的可以直接看后面的视频剪裁部分

录制配置:

@interface WKMovieRecorder ()
<
AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate,
WKMovieWriterDelegate
>

{
  AVCaptureSession* _session;
  AVCaptureVideoPreviewLayer* _preview;
  WKMovieWriter* _writer;
  //暂停录制
  BOOL _isCapturing;
  BOOL _isPaused;
  BOOL _discont;
  int _currentFile;
  CMTime _timeOffset;
  CMTime _lastVideo;
  CMTime _lastAudio;

  NSTimeInterval _maxDuration;
}

// Session management.
@property (nonatomic, strong) dispatch_queue_t sessionQueue;
@property (nonatomic, strong) dispatch_queue_t videoDataOutputQueue;
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureDevice *captureDevice;
@property (nonatomic, strong) AVCaptureDeviceInput *videoDeviceInput;
@property (nonatomic, strong) AVCaptureStillImageOutput *stillImageOutput;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@property (nonatomic, strong) AVCaptureConnection *audioConnection;
@property (nonatomic, strong) NSDictionary *videoCompressionSettings;
@property (nonatomic, strong) NSDictionary *audioCompressionSettings;
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *adaptor;
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;


//Utilities
@property (nonatomic, strong) NSMutableArray *frames;//存储录制帧
@property (nonatomic, assign) CaptureAVSetupResult result;
@property (atomic, readwrite) BOOL isCapturing;
@property (atomic, readwrite) BOOL isPaused;
@property (nonatomic, strong) NSTimer *durationTimer;

@property (nonatomic, assign) WKRecorderFinishedReason finishReason;

@end

实例化方法:

+ (WKMovieRecorder *)sharedRecorder
{
  static WKMovieRecorder *recorder;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    recorder = [[WKMovieRecorder alloc] initWithMaxDuration:CGFLOAT_MAX];
  });

  return recorder;
}

- (instancetype)initWithMaxDuration:(NSTimeInterval)duration
{
  if(self = [self init]){
    _maxDuration = duration;
    _duration = 0.f;
  }

  return self;
}

- (instancetype)init
{
  self = [super init];
  if (self) {
    _maxDuration = CGFLOAT_MAX;
    _duration = 0.f;
    _sessionQueue = dispatch_queue_create("wukong.movieRecorder.queue", DISPATCH_QUEUE_SERIAL );
    _videoDataOutputQueue = dispatch_queue_create( "wukong.movieRecorder.video", DISPATCH_QUEUE_SERIAL );
    dispatch_set_target_queue( _videoDataOutputQueue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
  }
  return self;
}

2.初始化设置
初始化设置分别为session创建、权限检查以及session配置
1).session创建
self.session = [[AVCaptureSession alloc] init];
self.result = CaptureAVSetupResultSuccess;

2).权限检查

//权限检查
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
      case AVAuthorizationStatusNotDetermined: {
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
          if (granted) {
            self.result = CaptureAVSetupResultSuccess;
          }
        }];
        break;
      }
      case AVAuthorizationStatusAuthorized: {

        break;
      }
      default:{
        self.result = CaptureAVSetupResultCameraNotAuthorized;
      }
    }

    if ( self.result != CaptureAVSetupResultSuccess) {

      if (self.authorizationResultBlock) {
        self.authorizationResultBlock(NO);
      }
      return;
    }

3).session配置
session配置是需要注意的是AVCaptureSession的配置不能在主线程, 需要自行创建串行线程。
3.1.1 获取输入设备与输入流

AVCaptureDevice *captureDevice = [[self class] deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];      
 _captureDevice = captureDevice;

 NSError *error = nil;
 _videoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&error];

 if (!_videoDeviceInput) {
  NSLog(@"未找到设备");
 }

3.1.2 录制帧数设置
帧数设置的主要目的是适配iPhone4,毕竟是应该淘汰的机器了

int frameRate;
      if ( [NSProcessInfo processInfo].processorCount == 1 )
      {
        if ([self.session canSetSessionPreset:AVCaptureSessionPresetLow]) {
          [self.session setSessionPreset:AVCaptureSessionPresetLow];
        }
        frameRate = 10;
      }else{
        if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480]) {
          [self.session setSessionPreset:AVCaptureSessionPreset640x480];
        }
        frameRate = 30;
      }

      CMTime frameDuration = CMTimeMake( 1, frameRate );

      if ( [_captureDevice lockForConfiguration:&error] ) {
        _captureDevice.activeVideoMaxFrameDuration = frameDuration;
        _captureDevice.activeVideoMinFrameDuration = frameDuration;
        [_captureDevice unlockForConfiguration];
      }
      else {
        NSLog( @"videoDevice lockForConfiguration returned error %@", error );
      }

3.1.3 视频输出设置
视频输出设置需要注意的问题是:要设置videoConnection的方向,这样才能保证设备旋转时的显示正常。

//Video
     if ([self.session canAddInput:_videoDeviceInput]) {

       [self.session addInput:_videoDeviceInput];
       self.videoDeviceInput = _videoDeviceInput;
       [self.session removeOutput:_videoDataOutput];

       AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
       _videoDataOutput = videoOutput;
       videoOutput.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA) };

       [videoOutput setSampleBufferDelegate:self queue:_videoDataOutputQueue];

       videoOutput.alwaysDiscardsLateVideoFrames = NO;

       if ( [_session canAddOutput:videoOutput] ) {
         [_session addOutput:videoOutput];

         [_captureDevice addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:FocusAreaChangedContext];

         _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];

         if(_videoConnection.isVideoStabilizationSupported){
           _videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
         }


         UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation;
         AVCaptureVideoOrientation initialVideoOrientation = AVCaptureVideoOrientationPortrait;
         if ( statusBarOrientation != UIInterfaceOrientationUnknown ) {
           initialVideoOrientation = (AVCaptureVideoOrientation)statusBarOrientation;
         }

         _videoConnection.videoOrientation = initialVideoOrientation;
       }

     }
     else{
       NSLog(@"无法添加视频输入到会话");
     }

3.1.4 音频设置
需要注意的是为了不丢帧,需要把音频输出的回调队列放在串行队列中

//audio
      AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
      AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];


      if ( ! audioDeviceInput ) {
        NSLog( @"Could not create audio device input: %@", error );
      }

      if ( [self.session canAddInput:audioDeviceInput] ) {
        [self.session addInput:audioDeviceInput];

      }
      else {
        NSLog( @"Could not add audio device input to the session" );
      }

      AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
      // Put audio on its own queue to ensure that our video processing doesn't cause us to drop audio
      dispatch_queue_t audioCaptureQueue = dispatch_queue_create( "wukong.movieRecorder.audio", DISPATCH_QUEUE_SERIAL );
      [audioOut setSampleBufferDelegate:self queue:audioCaptureQueue];

      if ( [self.session canAddOutput:audioOut] ) {
        [self.session addOutput:audioOut];
      }
      _audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];

还需要注意一个问题就是对于session的配置代码应该是这样的
[self.session beginConfiguration];

…配置代码

[self.session commitConfiguration];

由于篇幅问题,后面的录制代码我就挑重点的讲了。
3.2 视频存储
现在我们需要在AVCaptureVideoDataOutputSampleBufferDelegate与AVCaptureAudioDataOutputSampleBufferDelegate的回调中,将音频和视频写入沙盒。在这个过程中需要注意的,在启动session后获取到的第一帧黑色的,需要放弃。
3.2.1 创建WKMovieWriter类来封装视频存储操作
WKMovieWriter的主要作用是利用AVAssetWriter拿到CMSampleBufferRef,剪裁后再写入到沙盒中。
这是剪裁配置的代码,AVAssetWriter会根据cropSize来剪裁视频,这里需要注意的一个问题是cropSize的width必须是320的整数倍,不然的话剪裁出来的视频右侧会出现一条绿色的线

 NSDictionary *videoSettings;
if (_cropSize.height == 0 || _cropSize.width == 0) {

  _cropSize = [UIScreen mainScreen].bounds.size;

}

videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
         AVVideoCodecH264, AVVideoCodecKey,
         [NSNumber numberWithInt:_cropSize.width], AVVideoWidthKey,
         [NSNumber numberWithInt:_cropSize.height], AVVideoHeightKey,
         AVVideoScalingModeResizeAspectFill,AVVideoScalingModeKey,
         nil];

至此,视频录制就完成了。
接下来需要解决的预览的问题了

Part 2 卡顿问题解决
1.1 gif图生成
通过查资料发现了这篇blog 介绍说微信团队解决预览卡顿的问题使用的是播放图片gif,但是博客中的示例代码有问题,通过CoreAnimation来播放图片导致内存暴涨而crash。但是,还是给了我一些灵感,因为之前项目的启动页用到了gif图片的播放,所以我就想能不能把视频转成图片,然后再转成gif图进行播放,这样不就解决了问题了吗。于是我开始google功夫不负有心人找到了,图片数组转gif图片的方法。

gif图转换代码

static void makeAnimatedGif(NSArray *images, NSURL *gifURL, NSTimeInterval duration) {
  NSTimeInterval perSecond = duration /images.count;

  NSDictionary *fileProperties = @{
                   (__bridge id)kCGImagePropertyGIFDictionary: @{
                       (__bridge id)kCGImagePropertyGIFLoopCount: @0, // 0 means loop forever
                       }
                   };

  NSDictionary *frameProperties = @{
                   (__bridge id)kCGImagePropertyGIFDictionary: @{
                       (__bridge id)kCGImagePropertyGIFDelayTime: @(perSecond), // a float (not double!) in seconds, rounded to centiseconds in the GIF data
                       }
                   };

  CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)gifURL, kUTTypeGIF, images.count, NULL);
  CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);

  for (UIImage *image in images) {
    @autoreleasepool {

      CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
    }
  }

  if (!CGImageDestinationFinalize(destination)) {
    NSLog(@"failed to finalize image destination");
  }else{


  }
  CFRelease(destination);
}

转换是转换成功了,但是出现了新的问题,使用ImageIO生成gif图片时会导致内存暴涨,瞬间涨到100M以上,如果多个gif图同时生成的话一样会crash掉,为了解决这个问题需要用一个串行队列来进行gif图的生成  

1.2 视频转换为UIImages
主要是通过AVAssetReader、AVAssetTrack、AVAssetReaderTrackOutput 来进行转换

//转成UIImage
- (void)convertVideoUIImagesWithURL:(NSURL *)url finishBlock:(void (^)(id images, NSTimeInterval duration))finishBlock
{
    AVAsset *asset = [AVAsset assetWithURL:url];
    NSError *error = nil;
    self.reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];

    NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
    __weak typeof(self)weakSelf = self;
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_async(backgroundQueue, ^{
      __strong typeof(weakSelf) strongSelf = weakSelf;
      NSLog(@"");


      if (error) {
        NSLog(@"%@", [error localizedDescription]);

      }

      NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];

      AVAssetTrack *videoTrack =[videoTracks firstObject];
      if (!videoTrack) {
        return ;
      }
      int m_pixelFormatType;
      //   视频播放时,
      m_pixelFormatType = kCVPixelFormatType_32BGRA;
      // 其他用途,如视频压缩
      //  m_pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;

      NSMutableDictionary *options = [NSMutableDictionary dictionary];
      [options setObject:@(m_pixelFormatType) forKey:(id)kCVPixelBufferPixelFormatTypeKey];
      AVAssetReaderTrackOutput *videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];

      if ([strongSelf.reader canAddOutput:videoReaderOutput]) {

        [strongSelf.reader addOutput:videoReaderOutput];
      }
      [strongSelf.reader startReading];


      NSMutableArray *images = [NSMutableArray array];
      // 要确保nominalFrameRate>0,之前出现过android拍的0帧视频
      while ([strongSelf.reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
         @autoreleasepool {
        // 读取 video sample
        CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];

        if (!videoBuffer) {
          break;
        }

        [images addObject:[WKVideoConverter convertSampleBufferRefToUIImage:videoBuffer]];

        CFRelease(videoBuffer);
      }


     }
      if (finishBlock) {
        dispatch_async(dispatch_get_main_queue(), ^{
          finishBlock(images, duration);
        });
      }
    });


}

在这里有一个值得注意的问题,在视频转image的过程中,由于转换时间很短,在短时间内videoBuffer不能够及时得到释放,在多个视频同时转换时任然会出现内存问题,这个时候就需要用autoreleasepool来实现及时释放

@autoreleasepool {
 // 读取 video sample
 CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
   if (!videoBuffer) {
   break;
   }

   [images addObject:[WKVideoConverter convertSampleBufferRefToUIImage:videoBuffer]];
    CFRelease(videoBuffer); }

至此,微信小视频的难点(我认为的)就解决了,至于其他的实现代码请看demo就基本实现了,demo可以从这里下载。

视频暂停录制 http://www.gdcl.co.uk/2013/02/20/iPhone-Pause.html
视频crop绿边解决 http://stackoverflow.com/questions/22883525/avassetexportsession-giving-me-a-green-border-on-right-and-bottom-of-output-vide
视频裁剪:http://stackoverflow.com/questions/15737781/video-capture-with-11-aspect-ratio-in-ios/16910263#16910263
CMSampleBufferRef转image https://developer.apple.com/library/ios/qa/qa1702/_index.html
微信小视频分析 http://www.jianshu.com/p/3d5ccbde0de1

感谢以上文章的作者

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。