iOS,위 챗 모멘트 영상 캡 처 기능 구현

서언
위 챗 은 현재 이렇게 보급 되 고 기능 도 점점 강해 지고 있 습 니 다.여러분 들 은 위 챗 친구 들 이 동 영상 을 캡 처 하 는 기능 이나 애플 이 동 영상 을 찍 는 것 이 동 영상 편집 기능 에 대해 잘 알 고 있 는 지 모 르 겠 습 니 다.(작가 님 은 여기 서도 위 챗 의 이 기능 도 애플 을 모방 한 것 이 라 고 추측 합 니 다)이 기능 은 확실히 편리 하고 실 용적 인 것 같 습 니 다.최근 에 작가 도 음성 영상 기능 을 연구 하고 있 기 때문에 이 기능 을 실현 하 였 습 니 다.
기능 은 사실 보기 에는 매우 간단 하고 실현 과정 도 적지 않 은 구 덩이 를 밟 았 다.한편 으로 는 기록 하기;다른 한편 으로 는 실현 과정 에 대한 재 정리 라 고 할 수 있 습 니 다.그러면 코드 를 보 셔 도 잘 알 수 있 습 니 다.
효과.
일단 제 가 이 룬 효 과 를 보도 록 하 겠 습 니 다.
 
이루어지다
실현 과정 분석
전체 기능 은 세 부분 으로 나 눌 수 있다.
비디오 재생
이 부분 은 저희 가 영상 재생 기 를 따로 봉 하면 돼 요.
아래 의 미끄럼 보기이 부분 은 실현 과정 이 비교적 복잡 해서 모두 네 부분 으로 나 뉘 었 다.회색 커버,좌우 손잡이 슬라이더,슬라이더 중간 상하 두 줄,이미지 관리 보기
컨트롤 러 보기 논리 조립 및 기능 구현
비디오 플레이어 의 패키지
여기 서 AVPlayer,playerLayer,AVPlayerItem 등 세 가지 유형 을 사용 하여 영상 재생 기능 을 실현 했다.전체 사건 이 KVO 감청 에 기반 한 것 이기 때문에 Block 코드 를 추가 해 대외 감청 사용 을 제공 했다.

#import "FOFMoviePlayer.h"
@interface FOFMoviePlayer()
{
  AVPlayerLooper *_playerLooper;
  AVPlayerItem *_playItem;
  BOOL _loop;
}
@property(nonatomic,strong)NSURL *url;
@property(nonatomic,strong)AVPlayer *player;
@property(nonatomic,strong)AVPlayerLayer *playerLayer;
@property(nonatomic,strong)AVPlayerItem *playItem;
@property (nonatomic,assign) CMTime duration;
@end
@implementation FOFMoviePlayer
-(instancetype)initWithFrame:(CGRect)frame url:(NSURL *)url superLayer:(CALayer *)superLayer{
  self = [super init];
  if (self) {
    [self initplayers:superLayer];
    _playerLayer.frame = frame;
    self.url = url;
  }
  return self;
}
-(instancetype)initWithFrame:(CGRect)frame url:(NSURL *)url superLayer:(CALayer *)superLayer loop:(BOOL)loop{
  self = [self initWithFrame:frame url:url superLayer:superLayer];
  if (self) {
    _loop = loop;
  }
  return self;
}
- (void)initplayers:(CALayer *)superLayer{
  self.player = [[AVPlayer alloc] init];
  self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
  self.playerLayer.videoGravity = AVLayerVideoGravityResize;
  [superLayer addSublayer:self.playerLayer];
}
- (void)initLoopPlayers:(CALayer *)superLayer{
  self.player = [[AVQueuePlayer alloc] init];
  self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
  self.playerLayer.videoGravity = AVLayerVideoGravityResize;
  [superLayer addSublayer:self.playerLayer];
}
-(void)fof_play{
  [self.player play];
}
-(void)fof_pause{
  [self.player pause];
}
#pragma mark - Observe
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context{
  if ([keyPath isEqualToString:@"status"]) {
    AVPlayerItem *item = (AVPlayerItem *)object;
    AVPlayerItemStatus status = [[change objectForKey:@"new"] intValue]; //         
    if (status == AVPlayerItemStatusReadyToPlay) {
      _duration = item.duration;//           ,   AVPlayerItem        
      NSLog(@"    ");
      if (self.blockStatusReadyPlay) {
        self.blockStatusReadyPlay(item);
      }
    } else if (status == AVPlayerItemStatusFailed) {
      if (self.blockStatusFailed) {
        self.blockStatusFailed();
      }
      AVPlayerItem *item = (AVPlayerItem *)object;
      NSLog(@"%@",item.error);
      NSLog(@"AVPlayerStatusFailed");
    } else {
      self.blockStatusUnknown();
      NSLog(@"%@",item.error);
      NSLog(@"AVPlayerStatusUnknown");
    }
  }else if ([keyPath isEqualToString:@"tracking"]){
    NSInteger status = [change[@"new"] integerValue];
    if (self.blockTracking) {
      self.blockTracking(status);
    }
    if (status) {//    
      [self.player pause];
    }else{//    
    }
  }else if ([keyPath isEqualToString:@"loadedTimeRanges"]){
    NSArray *array = _playItem.loadedTimeRanges;
    CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//        
    CGFloat startSeconds = CMTimeGetSeconds(timeRange.start);
    CGFloat durationSeconds = CMTimeGetSeconds(timeRange.duration);
    NSTimeInterval totalBuffer = startSeconds + durationSeconds;//     
    double progress = totalBuffer/CMTimeGetSeconds(_duration);
    if (self.blockLoadedTimeRanges) {
      self.blockLoadedTimeRanges(progress);
    }
    NSLog(@"      :%f",totalBuffer);
  }else if ([keyPath isEqualToString:@"playbackBufferEmpty"]){
    NSLog(@"    ,    !");
  }else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]){
    if (self.blockPlaybackLikelyToKeepUp) {
      self.blockPlaybackLikelyToKeepUp([change[@"new"] boolValue]);
    }
  }
}
-(void)setUrl:(NSURL *)url{
  _url = url;
  [self.player replaceCurrentItemWithPlayerItem:self.playItem];
}
-(AVPlayerItem *)playItem{
  _playItem = [[AVPlayerItem alloc] initWithURL:_url];
  //        ,     、  、    
  [_playItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
  //         
  [_playItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
  //           ,          :
  [_playItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
  //               
  [_playItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(private_playerMovieFinish) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
  return _playItem;
}
- (void)private_playerMovieFinish{
  NSLog(@"    ");
  if (self.blockPlayToEndTime) {
    self.blockPlayToEndTime();
  }
  if (_loop) {//             
    [self.player pause];
    CMTime time = CMTimeMake(1, 1);
    __weak typeof(self)this = self;
    [self.player seekToTime:time completionHandler:^(BOOL finished) {
      [this.player play];
    }];
  }
}
-(void)dealloc{
  NSLog(@"-----  -----");
}
@end
동 영상 재생 기 는 중요 하지 않 습 니 다.저 자 는 동 영상 재생 기 에 관 한 것 을 단독으로 쓸 계획 입 니 다.
아래 미끄럼 보기
회색 가리개
회색 커버 는 쉬 워 요.여 기 는 작가 님 이 UIView 만 쓰 셨 어 요.

self.leftMaskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, height)];
self.leftMaskView.backgroundColor = [UIColor grayColor];
self.leftMaskView.alpha = 0.8;
[self addSubview:self.leftMaskView];
self.rightMaskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, height)];
self.rightMaskView.backgroundColor = [UIColor grayColor];
self.rightMaskView.alpha = 0.8;
슬라이더 중간 상하 두 줄
이 두 선 은 하나의 보기 라인 을 단독으로 봉 하여 처음에는 하나의 UIView 를 사용 하면 좋 겠 다 고 생각 했 지만 문 제 는 손잡이 의 미끄럼 이 선의 미끄럼 속도 와 일치 하지 않 고 선 이 비교적 느리다 는 것 이다.

@implementation Line
-(void)setBeginPoint:(CGPoint)beginPoint{
  _beginPoint = beginPoint;
  [self setNeedsDisplay];
}
-(void)setEndPoint:(CGPoint)endPoint{
  _endPoint = endPoint;
  [self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(context, 3);
  CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.9 alpha:1].CGColor);
  CGContextMoveToPoint(context, self.beginPoint.x, self.beginPoint.y);
  CGContextAddLineToPoint(context, self.endPoint.x, self.endPoint.y);
  CGContextStrokePath(context);
}
그림 관리 보기
손잡이,선,가 려 진 논 리 를 조립 하고 그림 을 표시 하 는 데 사용 되 는 VideoPieces 를 봉 했다.그림 이 10 장 밖 에 없 기 때문에 여 기 는 하나의 for 순환 으로 10 개의 UIImageView 가 추가 되 었 습 니 다.

@interface VideoPieces()
{
  CGPoint _beginPoint;
}
@property(nonatomic,strong) Haft *leftHaft;
@property(nonatomic,strong) Haft *rightHaft;
@property(nonatomic,strong) Line *topLine;
@property(nonatomic,strong) Line *bottomLine;
@property(nonatomic,strong) UIView *leftMaskView;
@property(nonatomic,strong) UIView *rightMaskView;
@end
@implementation VideoPieces
-(instancetype)initWithFrame:(CGRect)frame{
  self = [super initWithFrame:frame];
  if (self) {
    [self initSubViews:frame];
  }
  return self;
}
- (void)initSubViews:(CGRect)frame{
  CGFloat height = CGRectGetHeight(frame);
  CGFloat width = CGRectGetWidth(frame);
  CGFloat minGap = 30;
  CGFloat widthHaft = 10;
  CGFloat heightLine = 3;
  _leftHaft = [[Haft alloc] initWithFrame:CGRectMake(0, 0, widthHaft, height)];
  _leftHaft.alpha = 0.8;
  _leftHaft.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
  _leftHaft.rightEdgeInset = 20;
  _leftHaft.lefEdgeInset = 5;
  __weak typeof(self) this = self;
  [_leftHaft setBlockMove:^(CGPoint point) {
    CGFloat maxX = this.rightHaft.frame.origin.x-minGap;
    if (point.x=minX) {
      this.topLine.endPoint = CGPointMake(point.x-widthHaft, heightLine/2.0);
      this.bottomLine.endPoint = CGPointMake(point.x-widthHaft, heightLine/2.0);
      this.rightHaft.frame = CGRectMake(point.x, 0, widthHaft, height);
      this.rightMaskView.frame = CGRectMake(point.x+widthHaft, 0, width-point.x-widthHaft, height);
      if (this.blockSeekOffRight) {
        this.blockSeekOffRight(point.x);
      }
    }
  }];
  [_rightHaft setBlockMoveEnd:^{
    if (this.blockMoveEnd) {
      this.blockMoveEnd();
    }
  }];
  _topLine = [[Line alloc] init];
  _topLine.alpha = 0.8;
  _topLine.frame = CGRectMake(widthHaft, 0, width-2*widthHaft, heightLine);
  _topLine.beginPoint = CGPointMake(0, heightLine/2.0);
  _topLine.endPoint = CGPointMake(CGRectGetWidth(_topLine.bounds), heightLine/2.0);
  _topLine.backgroundColor = [UIColor clearColor];
  [self addSubview:_topLine];
  _bottomLine = [[Line alloc] init];
  _bottomLine.alpha = 0.8;
  _bottomLine.frame = CGRectMake(widthHaft, height-heightLine, width-2*widthHaft, heightLine);
  _bottomLine.beginPoint = CGPointMake(0, heightLine/2.0);
  _bottomLine.endPoint = CGPointMake(CGRectGetWidth(_bottomLine.bounds), heightLine/2.0);
  _bottomLine.backgroundColor = [UIColor clearColor];
  [self addSubview:_bottomLine];
  [self addSubview:_leftHaft];
  [self addSubview:_rightHaft];
  self.leftMaskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, height)];
  self.leftMaskView.backgroundColor = [UIColor grayColor];
  self.leftMaskView.alpha = 0.8;
  [self addSubview:self.leftMaskView];
  self.rightMaskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, height)];
  self.rightMaskView.backgroundColor = [UIColor grayColor];
  self.rightMaskView.alpha = 0.8;
  [self addSubview:self.rightMaskView];
}
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{
  UITouch *touch = touches.anyObject;
  _beginPoint = [touch locationInView:self];
}
손잡이 의 실현
손잡이 의 실현 은 여기 서 최적화 되 었 다.바로 미 끄 러 질 때 비교적 민감 하 다 는 것 이다.처음에 손가락 으로 미 끄 러 질 때 매우 민감 하지 않 고 자주 손가락 이 미 끄 러 졌 지만 손 을 움 직 이지 않 았 다.
민감 도 를 높이 는 방법 은 수신 이벤트 영역의 크기 를 늘 리 고 다시 쓰 는 것 입 니 다.-(BOOL)point Inside:(CGPoint)point with Event:(UIEvent*)event 이 방법 입 니 다.

@implementation Haft
-(instancetype)initWithFrame:(CGRect)frame{
  self = [super initWithFrame:frame];
  if (self) {
    self.userInteractionEnabled = true;
  }
  return self;
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
  CGRect rect = CGRectMake(self.bounds.origin.x-self.lefEdgeInset, self.bounds.origin.y-self.topEdgeInset, CGRectGetWidth(self.bounds)+self.lefEdgeInset+self.rightEdgeInset, CGRectGetHeight(self.bounds)+self.bottomEdgeInset+self.topEdgeInset);
  if (CGRectContainsPoint(rect, point)) {
    return YES;
  }
  return NO;
}
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{
  NSLog(@"  ");
}
-(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent *)event{
  NSLog(@"Move");
  UITouch *touch = touches.anyObject;
  CGPoint point = [touch locationInView:self.superview];
  CGFloat maxX = CGRectGetWidth(self.superview.bounds)-CGRectGetWidth(self.bounds);
  if (point.x>maxX) {
    point.x = maxX;
  }
  if (point.x>=0&&point.x<=(CGRectGetWidth(self.superview.bounds)-CGRectGetWidth(self.bounds))&&self.blockMove) {
    self.blockMove(point);
  }
}
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent *)event{
  if (self.blockMoveEnd) {
    self.blockMoveEnd();
  }
}
- (void)drawRect:(CGRect)rect {
  CGFloat width = CGRectGetWidth(self.bounds);
  CGFloat height = CGRectGetHeight(self.bounds);
  CGFloat lineWidth = 1.5;
  CGFloat lineHeight = 12;
  CGFloat gap = (width-lineWidth*2)/3.0;
  CGFloat lineY = (height-lineHeight)/2.0;
  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(context, lineWidth);
  CGContextSetStrokeColorWithColor(context, [[UIColor grayColor] colorWithAlphaComponent:0.8].CGColor);
  CGContextMoveToPoint(context, gap+lineWidth/2, lineY);
  CGContextAddLineToPoint(context, gap+lineWidth/2, lineY+lineHeight);
  CGContextStrokePath(context);
  CGContextSetLineWidth(context, lineWidth);
  CGContextSetStrokeColorWithColor(context, [[UIColor grayColor] colorWithAlphaComponent:0.8].CGColor);
  CGContextMoveToPoint(context, gap*2+lineWidth+lineWidth/2, lineY);
  CGContextAddLineToPoint(context, gap*2+lineWidth+lineWidth/2, lineY+lineHeight);
  CGContextStrokePath(context);
}
컨트롤 러 보기 논리 조립 및 기능 구현
이 부분의 논 리 는 가장 중요 하고 복잡 하 다.
미리 보기 그림 10 장 가 져 오기

- (NSArray *)getVideoThumbnail:(NSString *)path count:(NSInteger)count splitCompleteBlock:(void(^)(BOOL success, NSMutableArray *splitimgs))splitCompleteBlock {
  AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:path]];
  NSMutableArray *arrayImages = [NSMutableArray array];
  [asset loadValuesAsynchronouslyForKeys:@[@"duration"] completionHandler:^{
    AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
//    generator.maximumSize = CGSizeMake(480,136);//   CGSizeMake(480,136),        {240, 136}。        
    generator.appliesPreferredTrackTransform = YES;//                    。                       。
    /**     ,            。          ,             **/
    generator.requestedTimeToleranceAfter = kCMTimeZero;
    generator.requestedTimeToleranceBefore = kCMTimeZero;
    Float64 seconds = CMTimeGetSeconds(asset.duration);
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i
      CMTime time = CMTimeMakeWithSeconds(i*(seconds/10.0),1);//           
      [array addObject:[NSValue valueWithCMTime:time]];
    }
    __block int i = 0;
    [generator generateCGImagesAsynchronouslyForTimes:array completionHandler:^(CMTime requestedTime, CGImageRef _Nullable imageRef, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
      i++;
      if (result==AVAssetImageGeneratorSucceeded) {
        UIImage *image = [UIImage imageWithCGImage:imageRef];
        [arrayImages addObject:image];
      }else{
        NSLog(@"      !!!");
      }
      if (i==count) {
        dispatch_async(dispatch_get_main_queue(), ^{
          splitCompleteBlock(YES,arrayImages);
        });
      }
    }];
  }];
  return arrayImages;
}
10 장의 그림 은 쉽게 얻 을 수 있 지만 여기 서 주의해 야 할 것 은 리 셋 할 때 비동기 홈 팀 에 리 셋 해 야 합 니 다!그림 표시 지연 이 심 하지 않 으 면
좌우 슬라이더 사건 감청

[_videoPieces setBlockSeekOffLeft:^(CGFloat offX) {
  this.seeking = true;
  [this.moviePlayer fof_pause];
  this.lastStartSeconds = this.totalSeconds*offX/CGRectGetWidth(this.videoPieces.bounds);
  [this.moviePlayer.player seekToTime:CMTimeMakeWithSeconds(this.lastStartSeconds, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}];
[_videoPieces setBlockSeekOffRight:^(CGFloat offX) {
  this.seeking = true;
  [this.moviePlayer fof_pause];
  this.lastEndSeconds = this.totalSeconds*offX/CGRectGetWidth(this.videoPieces.bounds);
  [this.moviePlayer.player seekToTime:CMTimeMakeWithSeconds(this.lastEndSeconds, 1) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}];
좌우 슬라이더 의 이 벤트 를 감청 하여 오프셋 거 리 를 시간 으로 전환 하여 플레이어 의 시작 시간 과 종료 시간 을 설정 합 니 다.
반복 재생

self.timeObserverToken = [self.moviePlayer.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
  if (!this.seeking) {
    if (fabs(CMTimeGetSeconds(time)-this.lastEndSeconds)<=0.02) {
        [this.moviePlayer fof_pause];
        [this private_replayAtBeginTime:this.lastStartSeconds];
      }
  }
}];
여기에 두 가지 주의 점 이 있다.
1.addPeriodicTimeObserver ForInterval 을 방출 해 야 합 니 다.그렇지 않 으 면 메모리 가 누 출 될 수 있 습 니 다.

-(void)dealloc{
  [self.moviePlayer.player removeTimeObserver:self.timeObserverToken];
}
2.여기 서 재생 시간 을 감청 한 다음 에 우리 가 오른쪽 손 을 끌 어 당 기 는 시간 에 이 르 렀 는 지 계산 하고 이 르 면 다시 재생 합 니 다.이 문 제 는 작가 가 오랫동안 생각 했 는데,어떻게 하면 재생 하면 서 캡 처 할 수 있 습 니까?하마터면 잘못된 곳 에 들 어 갈 뻔 했 는데,정말 동 영상 을 캡 처 하 러 갔다.사실 여 기 는 동 영상 을 캡 처 하지 않 고 재생 시간 과 종료 시간 만 조절 하면 되 고 마지막 으로 한 번 만 캡 처 하면 된다.
총결산
이번 위 챗 영상 편집 이 이 뤄 지 는 과정 에서 작은 문제 가 많 았 던 것 은 사실이다.하지만 꼼꼼 한 연 구 를 통 해 완벽 하 게 이 뤄 져 무 거 운 짐 을 벗 은 듯 한 느낌 이 든다.하하.
소스 코드
GitHub 소스 코드
총결산
위 에서 말 한 것 은 편집장 님 께 서 소개 해 주신 iOS 가 위 챗 모멘트 영상 캡 처 기능 을 실현 하 는 것 입 니 다.여러분 께 도움 이 되 기 를 바 랍 니 다.궁금 한 점 이 있 으 시 면 메 시 지 를 남 겨 주세요.편집장 님 께 서 바로 답 해 드 리 겠 습 니 다.여기 서도 저희 사이트 에 대한 여러분 의 지지 에 감 사 드 립 니 다!

좋은 웹페이지 즐겨찾기