This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, TUICameraMediaType) {
TUICameraMediaTypePhoto = 1,
TUICameraMediaTypeVideo = 2,
};
typedef NS_ENUM(NSUInteger, TUICameraViewAspectRatio) {
TUICameraViewAspectRatio1x1,
TUICameraViewAspectRatio16x9,
TUICameraViewAspectRatio5x4,
};
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,32 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICameraManager : NSObject
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput;
- (id)resetFocusAndExposure:(AVCaptureDevice *)device;
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor;
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point;
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point;
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode;
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode;
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device;
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,174 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraManager.h"
@implementation TUICameraManager
#pragma mark - - Switch Camera
- (AVCaptureDeviceInput *)switchCamera:(AVCaptureSession *)session old:(AVCaptureDeviceInput *)oldinput new:(AVCaptureDeviceInput *)newinput {
[session beginConfiguration];
[session removeInput:oldinput];
if ([session canAddInput:newinput]) {
[session addInput:newinput];
[session commitConfiguration];
return newinput;
} else {
[session addInput:oldinput];
[session commitConfiguration];
return oldinput;
}
}
#pragma mark - - Zoom
- (id)zoom:(AVCaptureDevice *)device factor:(CGFloat)factor {
if (device.activeFormat.videoMaxZoomFactor > factor && factor >= 1.0) {
NSError *error;
if ([device lockForConfiguration:&error]) {
[device rampToVideoZoomFactor:factor withRate:4.0];
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Unsupported zoom factor" code:2000];
}
#pragma mark - - Focus
- (id)focus:(AVCaptureDevice *)device point:(CGPoint)point {
BOOL supported = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus];
if (supported) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.focusPointOfInterest = point;
device.focusMode = AVCaptureFocusModeAutoFocus;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Device does not support focus" code:2001];
}
#pragma mark - - Expose
static const NSString *CameraAdjustingExposureContext;
- (id)expose:(AVCaptureDevice *)device point:(CGPoint)point {
BOOL supported = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure];
if (supported) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.exposurePointOfInterest = point;
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&CameraAdjustingExposureContext];
}
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Device does not support exposure" code:2002];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == &CameraAdjustingExposureContext) {
AVCaptureDevice *device = (AVCaptureDevice *)object;
if (!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[object removeObserver:self forKeyPath:@"adjustingExposure" context:&CameraAdjustingExposureContext];
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if ([device lockForConfiguration:&error]) {
device.exposureMode = AVCaptureExposureModeLocked;
[device unlockForConfiguration];
} else {
NSLog(@"%@", error);
}
});
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - - Auto focus, exposure
- (id)resetFocusAndExposure:(AVCaptureDevice *)device {
AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
BOOL canResetFocus = [device isFocusPointOfInterestSupported] && [device isFocusModeSupported:focusMode];
BOOL canResetExposure = [device isExposurePointOfInterestSupported] && [device isExposureModeSupported:exposureMode];
CGPoint centerPoint = CGPointMake(0.5f, 0.5f);
NSError *error;
if ([device lockForConfiguration:&error]) {
if (canResetFocus) {
device.focusMode = focusMode;
device.focusPointOfInterest = centerPoint;
}
if (canResetExposure) {
device.exposureMode = exposureMode;
device.exposurePointOfInterest = centerPoint;
}
[device unlockForConfiguration];
}
return error;
}
#pragma mark - - Flash
- (AVCaptureFlashMode)flashMode:(AVCaptureDevice *)device {
return [device flashMode];
}
- (id)changeFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
if (![device hasFlash]) {
return [self error:@"Flash is not supported" code:2003];
}
if ([self torchMode:device] == AVCaptureTorchModeOn) {
[self setTorch:device model:AVCaptureTorchModeOff];
}
return [self setFlash:device mode:mode];
}
- (id)setFlash:(AVCaptureDevice *)device mode:(AVCaptureFlashMode)mode {
if ([device isFlashModeSupported:mode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.flashMode = mode;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Flash is not supported" code:2003];
}
#pragma mark - - Flashlight
- (AVCaptureTorchMode)torchMode:(AVCaptureDevice *)device {
return [device torchMode];
}
- (id)changeTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
if (![device hasTorch]) {
return [self error:@"Flashlight not supported" code:2004];
}
if ([self flashMode:device] == AVCaptureFlashModeOn) {
[self setFlash:device mode:AVCaptureFlashModeOff];
}
return [self setTorch:device model:mode];
}
- (id)setTorch:(AVCaptureDevice *)device model:(AVCaptureTorchMode)mode {
if ([device isTorchModeSupported:mode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.torchMode = mode;
[device unlockForConfiguration];
}
return error;
}
return [self error:@"Flashlight not supported" code:2004];
}
#pragma mark -
- (NSError *)error:(NSString *)text code:(NSInteger)code {
NSDictionary *desc = @{NSLocalizedDescriptionKey : text};
NSError *error = [NSError errorWithDomain:@"com.tui.camera" code:code userInfo:desc];
return error;
}
@end

View File

@@ -0,0 +1,86 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
#import "TUICameraMacro.h"
#import "TUICaptureVideoPreviewView.h"
NS_ASSUME_NONNULL_BEGIN
@class TUICameraView;
@protocol TUICameraViewDelegate <NSObject>
@optional
/**
* Flash
*/
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
* Fill light
*/
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Switch camera
*/
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Auto focus and exposure
*/
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *error))handle;
/**
*
* Foucus
*/
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
/**
*
* Expose
*/
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *error))handle;
/**
*
* Zoom
*/
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor;
- (void)cancelAction:(TUICameraView *)cameraView;
- (void)pictureLibAction:(TUICameraView *)cameraView;
- (void)takePhotoAction:(TUICameraView *)cameraView;
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration;
- (void)startRecordVideoAction:(TUICameraView *)cameraView;
- (void)didChangeTypeAction:(TUICameraView *)cameraView type:(TUICameraMediaType)type;
@end
@interface TUICameraView : UIView
@property(nonatomic, weak) id<TUICameraViewDelegate> delegate;
@property(nonatomic, readonly) TUICaptureVideoPreviewView *previewView;
/// default TUICameraMediaTypePhoto
@property(nonatomic) TUICameraMediaType type;
/// default TUICameraViewAspectRatio16x9
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
/// default 15s
@property(nonatomic, assign) CGFloat maxVideoCaptureTimeLimit;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,434 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraView.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import "TUICaptureTimer.h"
static CGFloat gPhotoBtnZoomInRatio = 1.125;
static CGFloat gProgressLayerLineWidth = 5.0;
@interface TUICameraView ()
@property(nonatomic) UIView *contentView;
@property(nonatomic) UIButton *switchCameraButton;
@property(nonatomic) UIButton *closeButton;
@property(nonatomic, strong) UIButton *pictureLibButton;
@property(nonatomic) UIView *focusView;
@property(nonatomic) UISlider *slider;
@property(nonatomic) UIView *photoBtn;
@property(nonatomic) UIView *photoStateView;
@property(nonatomic) UILongPressGestureRecognizer *longPress;
@property(nonatomic) CGRect lastRect;
@property(nonatomic) CAShapeLayer *progressLayer;
@property(nonatomic) TUICaptureTimer *timer;
@property(nonatomic) BOOL isVideoRecording;
@end
@implementation TUICameraView
@synthesize previewView = _previewView;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.type = TUICameraMediaTypePhoto;
self.aspectRatio = TUICameraViewAspectRatio16x9;
self.backgroundColor = [UIColor blackColor];
self.maxVideoCaptureTimeLimit = 15.0;
}
return self;
}
- (void)setupUI {
[self addSubview:self.contentView];
[self.contentView addSubview:self.previewView];
[self.contentView addSubview:self.switchCameraButton];
[self.contentView addSubview:self.photoBtn];
[self.contentView addSubview:self.closeButton];
[self.contentView addSubview:self.pictureLibButton];
[self.previewView addSubview:self.focusView];
[self.previewView addSubview:self.slider];
self.timer = ({
TUICaptureTimer *timer = [TUICaptureTimer new];
timer.maxCaptureTime = self.maxVideoCaptureTimeLimit;
__weak __typeof(self) weakSelf = self;
timer.progressBlock = ^(CGFloat ratio, CGFloat recordTime) {
weakSelf.progress = ratio;
};
timer.progressFinishBlock = ^(CGFloat ratio, CGFloat recordTime) {
weakSelf.progress = 1;
self.longPress.enabled = NO;
[weakSelf endVideoRecordWithCaptureDuration:recordTime];
self.longPress.enabled = YES;
};
timer.progressCancelBlock = ^{
weakSelf.progress = 0;
};
timer;
});
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[self.previewView addGestureRecognizer:tap];
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchAction:)];
[self.previewView addGestureRecognizer:pinch];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.bounds)) {
[self setupUI];
self.lastRect = self.bounds;
self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
CGFloat previewViewWidth = self.contentView.bounds.size.width;
CGFloat previewViewHeight;
switch (self.aspectRatio) {
case TUICameraViewAspectRatio1x1:
previewViewHeight = previewViewWidth;
break;
case TUICameraViewAspectRatio16x9:
previewViewHeight = previewViewWidth * (16.0 / 9.0);
break;
case TUICameraViewAspectRatio5x4:
previewViewHeight = previewViewWidth * (5.0 / 4.0);
break;
default:
break;
}
CGFloat previewViewY = (self.contentView.bounds.size.height - previewViewHeight) / 2.0;
self.previewView.frame = CGRectMake(0, previewViewY, self.contentView.bounds.size.width, previewViewHeight);
CGFloat switchCameraButtonWidth = 44.0;
self.switchCameraButton.frame =
CGRectMake(self.contentView.bounds.size.width - switchCameraButtonWidth - 16.0, 30.0, switchCameraButtonWidth, switchCameraButtonWidth);
if (isRTL()) {
[self.switchCameraButton resetFrameToFitRTL];
}
CGFloat photoBtnWidth = 100.0;
self.photoBtn.frame = CGRectMake((self.contentView.bounds.size.width - photoBtnWidth) / 2.0, self.contentView.bounds.size.height - photoBtnWidth - 30,
photoBtnWidth, photoBtnWidth);
self.photoBtn.layer.cornerRadius = photoBtnWidth / 2.0;
CGFloat distanceToPhotoBtn = 10.0;
CGFloat photoStateViewWidth = photoBtnWidth - 2 * distanceToPhotoBtn;
self.photoStateView.frame = CGRectMake(distanceToPhotoBtn, distanceToPhotoBtn, photoStateViewWidth, photoStateViewWidth);
self.photoStateView.layer.cornerRadius = photoStateViewWidth / 2.0;
if (self.type == TUICameraMediaTypeVideo) {
self.progressLayer.frame = CGRectInset(self.photoBtn.bounds, gProgressLayerLineWidth / 2.0, gProgressLayerLineWidth / 2.0);
CGFloat radius = self.progressLayer.bounds.size.width / 2;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:-M_PI_2
endAngle:-M_PI_2 + M_PI * 2
clockwise:YES];
self.progressLayer.path = path.CGPath;
[self.photoBtn.layer addSublayer:self.progressLayer];
}
CGFloat closeButtonWidth = 30.0;
CGFloat closeButtonX = (self.photoBtn.frame.origin.x - closeButtonWidth) / 2.0;
CGFloat closeButtonY = self.photoBtn.center.y - closeButtonWidth / 2.0;
self.closeButton.frame = CGRectMake(closeButtonX, closeButtonY, closeButtonWidth, closeButtonWidth);
if (isRTL()) {
[self.closeButton resetFrameToFitRTL];
}
CGFloat pictureButtonWidth = 30.0;
self.pictureLibButton.frame = CGRectMake(self.contentView.frame.size.width - closeButtonX, closeButtonY, pictureButtonWidth, pictureButtonWidth);
if (isRTL()) {
[self.pictureLibButton resetFrameToFitRTL];
}
self.slider.transform = CGAffineTransformMakeRotation(M_PI_2);
self.slider.frame = CGRectMake(self.bounds.size.width - 50, 50, 15, 200);
}
}
#pragma mark -
- (void)setProgress:(CGFloat)progress {
if (progress < 0) {
return;
} else if (progress < 1.0) {
self.progressLayer.strokeEnd = progress;
}
if (progress >= 1.0) {
self.progressLayer.strokeEnd = 1.0;
}
}
#pragma mark - Event Response
- (void)tapAction:(UIGestureRecognizer *)tap {
if ([_delegate respondsToSelector:@selector(focusAction:point:handle:)]) {
CGPoint point = [tap locationInView:self.previewView];
[self runFocusAnimation:self.focusView point:point];
[_delegate focusAction:self
point:[self.previewView captureDevicePointForPoint:point]
handle:^(NSError *error) {
if (error) NSAssert1(NO, @"%@", error); //[self showError:error];
}];
}
}
- (void)runFocusAnimation:(UIView *)view point:(CGPoint)point {
view.center = point;
view.hidden = NO;
[UIView animateWithDuration:0.15f
delay:0.0f
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
view.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1.0);
}
completion:^(BOOL complete) {
double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
view.hidden = YES;
view.transform = CGAffineTransformIdentity;
});
}];
}
- (void)pinchAction:(UIPinchGestureRecognizer *)pinch {
if ([_delegate respondsToSelector:@selector(zoomAction:factor:)]) {
if (pinch.state == UIGestureRecognizerStateBegan) {
[UIView animateWithDuration:0.1
animations:^{
self->_slider.alpha = 1;
}];
} else if (pinch.state == UIGestureRecognizerStateChanged) {
if (pinch.velocity > 0) {
_slider.value += pinch.velocity / 100;
} else {
_slider.value += pinch.velocity / 20;
}
[_delegate zoomAction:self factor:powf(5, _slider.value)];
} else {
[UIView animateWithDuration:0.1
animations:^{
self->_slider.alpha = 0.0;
}];
}
}
}
- (void)switchCameraButtonClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(swicthCameraAction:handle:)]) {
[self.delegate swicthCameraAction:self
handle:^(NSError *_Nonnull error){
//
}];
}
}
- (void)closeButtonClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(cancelAction:)]) {
[self.delegate cancelAction:self];
}
}
- (void)pictureLibClick:(UIButton *)btn {
if ([self.delegate respondsToSelector:@selector(pictureLibAction:)]) {
[self.delegate pictureLibAction:self];
}
}
- (void)longPressGesture:(UILongPressGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
[self beginVideoRecord];
break;
}
case UIGestureRecognizerStateChanged: {
break;
}
case UIGestureRecognizerStateEnded: {
[self endVideoRecordWithCaptureDuration:self.timer.captureDuration];
break;
}
default:
break;
}
}
- (void)beginVideoRecord {
if (self.isVideoRecording) {
return;
}
self.closeButton.hidden = YES;
self.isVideoRecording = YES;
self.pictureLibButton.hidden = YES;
[self.timer startTimer];
dispatch_async(dispatch_get_main_queue(), ^{
self.progressLayer.strokeEnd = 0.0;
[UIView animateWithDuration:0.2
animations:^{
self.photoStateView.transform = CGAffineTransformMakeScale(.5, .5);
self.photoBtn.transform = CGAffineTransformMakeScale(gPhotoBtnZoomInRatio, gPhotoBtnZoomInRatio);
}];
if ([self.delegate respondsToSelector:@selector(startRecordVideoAction:)]) {
[self.delegate startRecordVideoAction:self];
}
});
}
- (void)endVideoRecordWithCaptureDuration:(CGFloat)duration {
if (self.isVideoRecording == NO) {
return;
}
self.closeButton.hidden = NO;
self.isVideoRecording = NO;
self.pictureLibButton.hidden = NO;
[self.timer stopTimer];
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.2
animations:^{
self.photoStateView.transform = CGAffineTransformIdentity;
self.photoBtn.transform = CGAffineTransformIdentity;
}];
if ([self.delegate respondsToSelector:@selector(stopRecordVideoAction:RecordDuration:)]) {
[self.delegate stopRecordVideoAction:self RecordDuration:duration];
}
self.progressLayer.strokeEnd = 0.0;
});
}
- (void)tapGesture:(UITapGestureRecognizer *)tapGesture {
if ([_delegate respondsToSelector:@selector(takePhotoAction:)]) {
[_delegate takePhotoAction:self];
}
}
#pragma mark - Getters & Setters
- (UIView *)contentView {
if (!_contentView) {
_contentView = [UIView new];
}
return _contentView;
}
- (TUICaptureVideoPreviewView *)previewView {
if (!_previewView) {
_previewView = [[TUICaptureVideoPreviewView alloc] init];
_previewView.userInteractionEnabled = YES;
}
return _previewView;
}
- (UIButton *)switchCameraButton {
if (!_switchCameraButton) {
_switchCameraButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *switchCameraButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_switchCamera")];
[_switchCameraButton setImage:switchCameraButtonImage forState:UIControlStateNormal];
[_switchCameraButton addTarget:self action:@selector(switchCameraButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _switchCameraButton;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *closeButtonImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_back")];
[_closeButton setBackgroundImage:[closeButtonImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
[_closeButton addTarget:self action:@selector(closeButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
- (UIButton *)pictureLibButton {
if (!_pictureLibButton) {
_pictureLibButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *pictureImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"more_picture")];
[_pictureLibButton setBackgroundImage:pictureImage forState:UIControlStateNormal];
[_pictureLibButton addTarget:self action:@selector(pictureLibClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _pictureLibButton;
}
- (UIView *)photoBtn {
if (!_photoBtn) {
_photoBtn = [UIView new];
[_photoBtn setBackgroundColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5]];
if (self.type == TUICameraMediaTypeVideo) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
[_photoBtn addGestureRecognizer:longPress];
_longPress = longPress;
}
if (self.type == TUICameraMediaTypePhoto) {
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
[_photoBtn addGestureRecognizer:tapGesture];
}
_photoBtn.userInteractionEnabled = YES;
_photoStateView = [UIView new];
_photoStateView.backgroundColor = [UIColor whiteColor];
[_photoBtn addSubview:_photoStateView];
}
return _photoBtn;
}
- (CAShapeLayer *)progressLayer {
if (!_progressLayer) {
_progressLayer = [CAShapeLayer layer];
_progressLayer.fillColor = [UIColor clearColor].CGColor;
_progressLayer.lineWidth = gProgressLayerLineWidth;
_progressLayer.strokeColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
_progressLayer.strokeStart = 0;
_progressLayer.strokeEnd = 0;
_progressLayer.lineCap = kCALineCapButt;
}
return _progressLayer;
}
- (UIView *)focusView {
if (_focusView == nil) {
_focusView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150, 150.0f)];
_focusView.backgroundColor = [UIColor clearColor];
_focusView.layer.borderColor = [UIColor colorWithRed:0 green:204.0 / 255 blue:0 alpha:1].CGColor;
_focusView.layer.borderWidth = 3.0f;
_focusView.hidden = YES;
}
return _focusView;
}
- (UISlider *)slider {
if (_slider == nil) {
_slider = [[UISlider alloc] init];
_slider.minimumValue = 0;
_slider.maximumValue = 1;
_slider.maximumTrackTintColor = [UIColor whiteColor];
_slider.minimumTrackTintColor = [UIColor whiteColor];
_slider.alpha = 0.0;
_slider.hidden = YES;
}
return _slider;
}
@end

View File

@@ -0,0 +1,32 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
@import UIKit;
#import "TUICameraMacro.h"
@class TUICameraViewController;
@protocol TUICameraViewControllerDelegate <NSObject>
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url;
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data;
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller;
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback;
@end
@interface TUICameraViewController : UIViewController
@property(nonatomic, weak) id<TUICameraViewControllerDelegate> delegate;
/// default TUICameraMediaTypePhoto
@property(nonatomic) TUICameraMediaType type;
/// default TUICameraViewAspectRatio16x9
@property(nonatomic) TUICameraViewAspectRatio aspectRatio;
/// default 15s
@property(nonatomic) NSTimeInterval videoMaximumDuration;
/// default 3s
@property(nonatomic) NSTimeInterval videoMinimumDuration;
@end

View File

@@ -0,0 +1,413 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICameraViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <CoreMedia/CMMetadata.h>
#import <Photos/Photos.h>
#import "TUICameraView.h"
#import "TUICaptureImagePreviewController.h"
#import "TUICaptureVideoPreviewViewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
#import "TUICameraManager.h"
#import "TUICaptureTimer.h"
#import "TUIMotionManager.h"
#import "TUIMovieManager.h"
@interface TUICameraViewController () <AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate, TUICameraViewDelegate> {
AVCaptureSession *_session;
AVCaptureDeviceInput *_deviceInput;
AVCaptureConnection *_videoConnection;
AVCaptureConnection *_audioConnection;
AVCaptureVideoDataOutput *_videoOutput;
AVCaptureStillImageOutput *_imageOutput;
BOOL _recording;
}
@property(nonatomic, strong) TUICameraView *cameraView;
@property(nonatomic, strong) TUIMovieManager *movieManager;
@property(nonatomic, strong) TUICameraManager *cameraManager;
@property(nonatomic, strong) TUIMotionManager *motionManager;
@property(nonatomic, strong) AVCaptureDevice *activeCamera;
@property(nonatomic, strong) AVCaptureDevice *inactiveCamera;
@property(nonatomic) BOOL isFirstShow;
@property(nonatomic) BOOL lastPageBarHidden;
@end
@implementation TUICameraViewController
- (instancetype)init {
self = [super init];
if (self) {
_motionManager = [[TUIMotionManager alloc] init];
_cameraManager = [[TUICameraManager alloc] init];
_type = TUICameraMediaTypePhoto;
_aspectRatio = TUICameraViewAspectRatio16x9;
_videoMaximumDuration = 15.0;
_videoMinimumDuration = 3.0;
_isFirstShow = YES;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.cameraView = [[TUICameraView alloc] initWithFrame:self.view.bounds];
self.cameraView.type = self.type;
self.cameraView.aspectRatio = self.aspectRatio;
self.cameraView.delegate = self;
self.cameraView.maxVideoCaptureTimeLimit = self.videoMaximumDuration;
[self.view addSubview:self.cameraView];
NSError *error;
[self setupSession:&error];
if (!error) {
[self.cameraView.previewView setCaptureSessionsion:_session];
[self startCaptureSession];
} else {
// NSAssert1(NO, @"Camera Initialize Failed : %@", error.localizedDescription);
// [self showErrorStr:error.localizedDescription];
}
}
- (void)dealloc {
[self stopCaptureSession];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.isFirstShow) {
self.isFirstShow = NO;
self.lastPageBarHidden = self.navigationController.navigationBarHidden;
}
self.navigationController.navigationBarHidden = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
if (!parent) {
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
}
}
#pragma mark - - Input Device
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
- (AVCaptureDevice *)activeCamera {
return _deviceInput.device;
}
- (AVCaptureDevice *)inactiveCamera {
AVCaptureDevice *device = nil;
if ([[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count] > 1) {
if ([self activeCamera].position == AVCaptureDevicePositionBack) {
device = [self cameraWithPosition:AVCaptureDevicePositionFront];
} else {
device = [self cameraWithPosition:AVCaptureDevicePositionBack];
}
}
return device;
}
#pragma mark - - Configuration
- (void)setupSession:(NSError **)error {
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetHigh;
[self setupSessionInputs:error];
[self setupSessionOutputs:error];
}
- (void)setupSessionInputs:(NSError **)error {
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
if ([_session canAddInput:videoInput]) {
[_session addInput:videoInput];
}
}
_deviceInput = videoInput;
if (_type == TUICameraMediaTypeVideo) {
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioIn = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:error];
if ([_session canAddInput:audioIn]) {
[_session addInput:audioIn];
}
}
}
- (void)setupSessionOutputs:(NSError **)error {
dispatch_queue_t captureQueue = dispatch_queue_create("com.tui.captureQueue", DISPATCH_QUEUE_SERIAL);
AVCaptureVideoDataOutput *videoOut = [[AVCaptureVideoDataOutput alloc] init];
[videoOut setAlwaysDiscardsLateVideoFrames:YES];
[videoOut setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]}];
[videoOut setSampleBufferDelegate:self queue:captureQueue];
if ([_session canAddOutput:videoOut]) {
[_session addOutput:videoOut];
}
_videoOutput = videoOut;
_videoConnection = [videoOut connectionWithMediaType:AVMediaTypeVideo];
if (_type == TUICameraMediaTypeVideo) {
AVCaptureAudioDataOutput *audioOut = [[AVCaptureAudioDataOutput alloc] init];
[audioOut setSampleBufferDelegate:self queue:captureQueue];
if ([_session canAddOutput:audioOut]) {
[_session addOutput:audioOut];
}
_audioConnection = [audioOut connectionWithMediaType:AVMediaTypeAudio];
}
AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];
imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};
if ([_session canAddOutput:imageOutput]) {
[_session addOutput:imageOutput];
}
_imageOutput = imageOutput;
}
#pragma mark - - Session Control
- (void)startCaptureSession {
if (!_session.isRunning) {
[_session startRunning];
}
}
- (void)stopCaptureSession {
if (_session.isRunning) {
[_session stopRunning];
}
}
#pragma mark - - Camera Operation
- (void)zoomAction:(TUICameraView *)cameraView factor:(CGFloat)factor {
NSError *error = [_cameraManager zoom:[self activeCamera] factor:factor];
if (error) NSLog(@"%@", error);
}
- (void)focusAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager focus:[self activeCamera] point:point];
handle(error);
NSLog(@"%f", [self activeCamera].activeFormat.videoMaxZoomFactor);
}
- (void)exposAction:(TUICameraView *)cameraView point:(CGPoint)point handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager expose:[self activeCamera] point:point];
handle(error);
}
- (void)autoFocusAndExposureAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
NSError *error = [_cameraManager resetFocusAndExposure:[self activeCamera]];
handle(error);
}
- (void)flashLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
BOOL on = [_cameraManager flashMode:[self activeCamera]] == AVCaptureFlashModeOn;
AVCaptureFlashMode mode = on ? AVCaptureFlashModeOff : AVCaptureFlashModeOn;
NSError *error = [_cameraManager changeFlash:[self activeCamera] mode:mode];
handle(error);
}
- (void)torchLightAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
BOOL on = [_cameraManager torchMode:[self activeCamera]] == AVCaptureTorchModeOn;
AVCaptureTorchMode mode = on ? AVCaptureTorchModeOff : AVCaptureTorchModeOn;
NSError *error = [_cameraManager changeTorch:[self activeCamera] model:mode];
handle(error);
}
- (void)swicthCameraAction:(TUICameraView *)cameraView handle:(void (^)(NSError *))handle {
NSError *error;
AVCaptureDevice *videoDevice = [self inactiveCamera];
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (videoInput) {
CATransition *animation = [CATransition animation];
animation.type = @"oglFlip";
animation.subtype = kCATransitionFromLeft;
animation.duration = 0.5;
[self.cameraView.previewView.layer addAnimation:animation forKey:@"flip"];
AVCaptureFlashMode mode = [_cameraManager flashMode:[self activeCamera]];
_deviceInput = [_cameraManager switchCamera:_session old:_deviceInput new:videoInput];
_videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
[_cameraManager changeFlash:[self activeCamera] mode:mode];
}
handle(error);
}
#pragma mark - - Taking Photo
- (void)takePhotoAction:(TUICameraView *)cameraView {
AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];
if (connection.isVideoOrientationSupported) {
connection.videoOrientation = [self currentVideoOrientation];
}
[_imageOutput captureStillImageAsynchronouslyFromConnection:connection
completionHandler:^(CMSampleBufferRef _Nullable imageDataSampleBuffer, NSError *_Nullable error) {
if (error) {
[self showErrorStr:error.localizedDescription];
return;
}
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
UIImage *image = [[UIImage alloc] initWithData:imageData];
TUICaptureImagePreviewController *vc = [[TUICaptureImagePreviewController alloc] initWithImage:image];
[self.navigationController pushViewController:vc animated:YES];
__weak __typeof(self) weakSelf = self;
vc.commitBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
UIGraphicsBeginImageContext(CGSizeMake(image.size.width, image.size.height));
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
UIImage *convertToUpImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *data = UIImageJPEGRepresentation(convertToUpImage, 0.75);
[strongSelf.delegate cameraViewController:strongSelf didFinishPickingMediaWithImageData:data];
[strongSelf popViewControllerAnimated:YES];
};
vc.cancelBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.navigationController popViewControllerAnimated:YES];
};
}];
}
- (void)cancelAction:(TUICameraView *)cameraView {
[self.delegate cameraViewControllerDidCancel:self];
[self popViewControllerAnimated:YES];
}
- (void)pictureLibAction:(TUICameraView *)cameraView {
@weakify(self);
[self.delegate cameraViewControllerDidPictureLib:self
finishCallback:^{
@strongify(self);
[self popViewControllerAnimated:NO];
}];
}
#pragma mark - - Record
- (void)startRecordVideoAction:(TUICameraView *)cameraView {
/**
* Recreate each time to avoid Crash caused by unreleased previous information
*/
_movieManager = [[TUIMovieManager alloc] init];
_recording = YES;
_movieManager.currentDevice = [self activeCamera];
_movieManager.currentOrientation = [self currentVideoOrientation];
@weakify(self);
[_movieManager start:^(NSError *_Nonnull error) {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (error) [self showErrorStr:error.localizedDescription];
});
}];
}
- (void)stopRecordVideoAction:(TUICameraView *)cameraView RecordDuration:(CGFloat)duration {
_recording = NO;
@weakify(self);
[_movieManager stop:^(NSURL *_Nonnull url, NSError *_Nonnull error) {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (duration < self.videoMinimumDuration) {
[self showErrorStr:TIMCommonLocalizableString(TUIKitMoreVideoCaptureDurationTip)];
} else if (error) {
[self showErrorStr:error.localizedDescription];
} else {
TUICaptureVideoPreviewViewController *videoPreviewController = [[TUICaptureVideoPreviewViewController alloc] initWithVideoURL:url];
[self.navigationController pushViewController:videoPreviewController animated:YES];
@weakify(self);
videoPreviewController.commitBlock = ^{
@strongify(self);
[self.delegate cameraViewController:self didFinishPickingMediaWithVideoURL:url];
[self popViewControllerAnimated:YES];
};
videoPreviewController.cancelBlock = ^{
@strongify(self);
[self.navigationController popViewControllerAnimated:YES];
};
}
});
}];
}
#pragma mark - - AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if (_recording) {
[_movieManager writeData:connection video:_videoConnection audio:_audioConnection buffer:sampleBuffer];
}
}
#pragma mark - - Others
- (AVCaptureVideoOrientation)currentVideoOrientation {
AVCaptureVideoOrientation orientation;
switch (self.motionManager.deviceOrientation) {
case UIDeviceOrientationPortrait:
orientation = AVCaptureVideoOrientationPortrait;
break;
case UIDeviceOrientationLandscapeLeft:
orientation = AVCaptureVideoOrientationLandscapeRight;
break;
case UIDeviceOrientationLandscapeRight:
orientation = AVCaptureVideoOrientationLandscapeLeft;
break;
case UIDeviceOrientationPortraitUpsideDown:
orientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
default:
orientation = AVCaptureVideoOrientationPortrait;
break;
}
return orientation;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)popViewControllerAnimated:(BOOL)animated {
NSUInteger index = [self.navigationController.viewControllers indexOfObject:self];
index--;
UIViewController *lastVC = nil;
if (index > 0 && index < self.navigationController.viewControllers.count) {
lastVC = self.navigationController.viewControllers[index];
}
self.navigationController.navigationBarHidden = self.lastPageBarHidden;
if (lastVC) {
[self.navigationController popToViewController:lastVC animated:animated];
} else {
[self.navigationController popViewControllerAnimated:animated];
}
}
- (void)showErrorStr:(NSString *)errStr {
[TUITool makeToast:errStr duration:1 position:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0)];
}
@end

View File

@@ -0,0 +1,18 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
@import UIKit;
@interface TUICaptureImagePreviewController : UIViewController
@property(nonatomic) void (^commitBlock)(void);
@property(nonatomic) void (^cancelBlock)(void);
- (instancetype)initWithImage:(UIImage *)image NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,91 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureImagePreviewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
@interface TUICaptureImagePreviewController () {
UIImage *_image;
}
@property(nonatomic) UIImageView *imageView;
@property(nonatomic) UIButton *commitButton;
@property(nonatomic) UIButton *cancelButton;
@property(nonatomic) CGRect lastRect;
@end
@implementation TUICaptureImagePreviewController
- (instancetype)initWithImage:(UIImage *)image {
if (self = [super initWithNibName:nil bundle:nil]) {
_image = image;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
UIImageView *imageView = [[UIImageView alloc] initWithImage:_image];
imageView.layer.masksToBounds = YES;
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
self.imageView = imageView;
NSLog(@"%ld--%ld", (long)_image.imageOrientation, UIImageOrientationUp);
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.commitButton];
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
[self.cancelButton setBackgroundImage:cancelButtonBGImage forState:UIControlStateNormal];
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.cancelButton];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
self.lastRect = self.view.bounds;
self.imageView.frame = self.view.bounds;
CGFloat commitButtonWidth = 80.0;
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
CGFloat cancelButtonX = commitButtonWidth;
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
if (isRTL()) {
[self.commitButton resetFrameToFitRTL];
[self.cancelButton resetFrameToFitRTL];
}
}
}
- (void)commitButtonClick:(UIButton *)btn {
if (self.commitBlock) {
self.commitBlock();
}
}
- (void)cancelButtonClick:(UIButton *)btn {
if (self.cancelBlock) {
self.cancelBlock();
}
}
@end

View File

@@ -0,0 +1,24 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICaptureTimer : NSObject
@property(nonatomic, assign) CGFloat maxCaptureTime;
@property(nonatomic, assign, readonly) CGFloat captureDuration;
@property(nonatomic, copy) void (^progressBlock)(CGFloat ratio, CGFloat recordTime);
@property(nonatomic, copy) void (^progressCancelBlock)(void);
@property(nonatomic, copy) void (^progressFinishBlock)(CGFloat ratio, CGFloat recordTime);
- (void)startTimer;
- (void)stopTimer;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,77 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureTimer.h"
@interface TUICaptureTimer ()
@property(nonatomic, strong) dispatch_source_t gcdTimer;
@property(nonatomic, assign) CGFloat captureDuration;
@end
@implementation TUICaptureTimer
- (instancetype)init {
self = [super init];
if (self) {
self.maxCaptureTime = 15.0f;
}
return self;
}
- (void)startTimer {
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
NSTimeInterval delayTime = 0.f;
NSTimeInterval timeInterval = 0.1f;
dispatch_time_t startDelayTime = dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(delayTime * NSEC_PER_SEC));
dispatch_source_set_timer(self.gcdTimer, startDelayTime, timeInterval * NSEC_PER_SEC, timeInterval * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.gcdTimer, ^{
self.captureDuration += timeInterval;
/**
* Updating UI on the main thread
*/
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressBlock) {
self.progressBlock(self.captureDuration / self.maxCaptureTime, self.captureDuration);
}
});
/**
* Fnish
*/
if (self.captureDuration >= self.maxCaptureTime) {
/**
* Invalid timer
*/
CGFloat ratio = self.captureDuration / self.maxCaptureTime;
CGFloat recordTime = self.captureDuration;
[self cancel];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressFinishBlock) self.progressFinishBlock(ratio, recordTime);
});
}
});
/**
* Start the task. After the GCD timer is created, it needs to be started manually
*/
dispatch_resume(self.gcdTimer);
}
- (void)stopTimer {
[self cancel];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.progressCancelBlock) self.progressCancelBlock();
});
}
- (void)cancel {
if (self.gcdTimer) {
dispatch_source_cancel(self.gcdTimer);
self.gcdTimer = nil;
}
self.captureDuration = 0;
}
@end

View File

@@ -0,0 +1,14 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
@interface TUICaptureVideoPreviewView : UIView
@property(strong, nonatomic) AVCaptureSession *captureSessionsion;
- (CGPoint)captureDevicePointForPoint:(CGPoint)point;
@end

View File

@@ -0,0 +1,37 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureVideoPreviewView.h"
@implementation TUICaptureVideoPreviewView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[(AVCaptureVideoPreviewLayer *)self.layer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
return self;
}
- (AVCaptureSession *)captureSessionsion {
return [(AVCaptureVideoPreviewLayer *)self.layer session];
}
- (void)setCaptureSessionsion:(AVCaptureSession *)session {
[(AVCaptureVideoPreviewLayer *)self.layer setSession:session];
}
- (CGPoint)captureDevicePointForPoint:(CGPoint)point {
AVCaptureVideoPreviewLayer *layer = (AVCaptureVideoPreviewLayer *)self.layer;
return [layer captureDevicePointOfInterestForPoint:point];
}
/**
* Make the layer method of the view return the AVCaptureVideoPreviewLayer class object
*/
+ (Class)layerClass {
return [AVCaptureVideoPreviewLayer class];
}
@end

View File

@@ -0,0 +1,22 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUICaptureVideoPreviewViewController : UIViewController
@property(nonatomic) void (^commitBlock)(void);
@property(nonatomic) void (^cancelBlock)(void);
- (instancetype)initWithVideoURL:(NSURL *)url NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,136 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUICaptureVideoPreviewViewController.h"
#import <TIMCommon/TIMCommonModel.h>
#import <TIMCommon/TIMDefine.h>
@import AVFoundation;
@interface TUICaptureVideoPreviewViewController ()
@property(nonatomic) NSURL *fileURL;
@property(nonatomic) AVPlayer *player;
@property(nonatomic) AVPlayerItem *item;
@property(nonatomic) AVPlayerLayer *playerLayer;
@property(nonatomic) UIButton *commitButton;
@property(nonatomic) UIButton *cancelButton;
@property(nonatomic) CGRect lastRect;
@property(nonatomic) BOOL onShow;
@property(nonatomic) BOOL onReadyToPlay;
@end
@implementation TUICaptureVideoPreviewViewController
- (instancetype)initWithVideoURL:(NSURL *)url {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_fileURL = url;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blackColor];
self.item = [[AVPlayerItem alloc] initWithURL:self.fileURL];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.item];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.view.layer addSublayer:self.playerLayer];
self.commitButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *commitImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commit")];
[self.commitButton setImage:commitImage forState:UIControlStateNormal];
UIImage *commitBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camer_commitBg")];
[self.commitButton setBackgroundImage:commitBGImage forState:UIControlStateNormal];
[self.commitButton addTarget:self action:@selector(commitButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.commitButton];
self.cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *cancelButtonBGImage = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"camera_cancel")];
[self.cancelButton setBackgroundImage:[cancelButtonBGImage rtl_imageFlippedForRightToLeftLayoutDirection] forState:UIControlStateNormal];
[self.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.cancelButton];
[self.item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.item];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
if (status == AVPlayerStatusReadyToPlay) {
self.onReadyToPlay = YES;
[self playVideo];
}
}
}
- (void)playVideo {
[TUITool dispatchMainAsync:^{
if (self.onShow && self.onReadyToPlay) {
[self.player play];
}
}];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.onShow = YES;
[self playVideo];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (!CGRectEqualToRect(self.lastRect, self.view.bounds)) {
self.lastRect = self.view.bounds;
self.playerLayer.frame = self.view.bounds;
CGFloat commitButtonWidth = 80.0;
CGFloat buttonDistance = (self.view.bounds.size.width - 2 * commitButtonWidth) / 3.0;
CGFloat commitButtonY = self.view.bounds.size.height - commitButtonWidth - 50.0;
CGFloat commitButtonX = 2 * buttonDistance + commitButtonWidth;
self.commitButton.frame = CGRectMake(commitButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
CGFloat cancelButtonX = commitButtonWidth;
self.cancelButton.frame = CGRectMake(cancelButtonX, commitButtonY, commitButtonWidth, commitButtonWidth);
if (isRTL()) {
[self.commitButton resetFrameToFitRTL];
[self.cancelButton resetFrameToFitRTL];
}
}
}
- (void)commitButtonClick:(UIButton *)btn {
if (self.commitBlock) {
[self removeObserver];
self.commitBlock();
}
}
- (void)cancelButtonClick:(UIButton *)btn {
if (self.cancelBlock) {
[self removeObserver];
self.cancelBlock();
}
}
- (void)playFinished:(NSNotification *)noti {
[self.player seekToTime:CMTimeMake(0, 1)];
[self.player play];
}
- (void)removeObserver {
[self.item removeObserver:self forKeyPath:@"status"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

View File

@@ -0,0 +1,57 @@
//
// TUIChatMediaDataProvider.h
// TUIChat
//
// Created by harvy on 2022/12/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <TIMCommon/TUIMessageCellData.h>
#import "TUIChatMediaSendingManager.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^TUIChatMediaDataProviderResultCallback)(BOOL success, NSString *__nullable message, NSString *__nullable path);
@protocol TUIChatMediaDataProtocol <NSObject>
- (void)selectPhoto;
- (void)takePicture;
- (void)takeVideo;
- (void)multimediaRecord;
- (void)selectFile;
@end
@protocol TUIChatMediaDataListener <NSObject>
- (void)onProvideImage:(NSString *)imageUrl;
- (void)onProvideImageError:(NSString *)errorMessage;
- (void)onProvideVideo:(NSString *)videoUrl
snapshot:(NSString *)snapshotUrl
duration:(NSInteger)duration
placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
- (void)onProvidePlaceholderVideoSnapshot:(NSString *)snapshotUrl
SnapImage:(UIImage *)img
Completion:(void (^__nullable)(BOOL finished, TUIMessageCellData *placeHolderCellData))completion;
- (void)onProvideVideoError:(NSString *)errorMessage;
- (void)onProvideFile:(NSString *)fileUrl filename:(NSString *)filename fileSize:(NSInteger)fileSize;
- (void)onProvideFileError:(NSString *)errorMessage;
- (NSString *)currentConversationID;
- (BOOL)isPageAppears;
- (void)sendPlaceHolderUIMessage:(TUIMessageCellData *)cellData;
- (void)sendMessage:(V2TIMMessage *)message placeHolderCellData:(TUIMessageCellData *)placeHolderCellData;
@end
@interface TUIChatMediaDataProvider : NSObject <TUIChatMediaDataProtocol>
@property(nonatomic, weak) UIViewController *presentViewController;
@property(nonatomic, weak) id<TUIChatMediaDataListener> listener;
@property(nonatomic, copy) NSString *conversationID;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,898 @@
//
// TUIChatMediaDataProvider.m
// TUIChat
//
// Created by harvy on 2022/12/20.
// Copyright © 2023 Tencent. All rights reserved.
//
#import "TUIChatMediaDataProvider.h"
#import <AssetsLibrary/AssetsLibrary.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <Photos/Photos.h>
#import <PhotosUI/PhotosUI.h>
#import <SDWebImage/SDWebImage.h>
#import <TIMCommon/TIMDefine.h>
#import <TIMCommon/TUIUserAuthorizationCenter.h>
#import <TIMCommon/NSTimer+TUISafe.h>
#import <TUICore/TUITool.h>
#import <TUICore/TUICore.h>
#import "TUICameraViewController.h"
#import "TUIChatConfig.h"
#import "AlbumPicker.h"
#import "MultimediaRecorder.h"
#define kTUIChatMediaSelectImageMax 9
@interface TUIChatMediaDataProvider () <PHPickerViewControllerDelegate,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate,
UIDocumentPickerDelegate,
TUICameraViewControllerDelegate>
@end
@implementation TUIChatMediaDataProvider
#pragma mark - Public API
- (void)selectPhoto {
if ([AlbumPicker sharedInstance].advancedAlbumPicker) {
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf.listener) strongListener = weakSelf.listener;
[[AlbumPicker sharedInstance].advancedAlbumPicker pickMediaWithCaller:self.presentViewController originalMediaPicked:^(NSDictionary *param) {
if (param) {
NSString * type = param[@"type"];
if ([type isEqualToString:@"image"]) {
// image do nothing
}
else if ([type isEqualToString:@"video"]) {
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
if ([strongListener respondsToSelector:@selector(sendPlaceHolderUIMessage:)]) {
[strongListener sendPlaceHolderUIMessage:placeHolderCellData];
}
TUIChatMediaTask * task = [[TUIChatMediaTask alloc] init];
task.placeHolderCellData = placeHolderCellData;
task.msgID = placeHolderCellData.msgID;
task.conversationID = weakSelf.conversationID;
if (placeHolderCellData.msgID.length > 0) {
[TUIChatMediaSendingManager.sharedInstance addMediaTask: task forKey:placeHolderCellData.msgID];
}
}
else {
// do nothing
}
}
} progressCallback:^(NSDictionary *param) {
NSLog(@"%@,strongListener:%@",param,strongListener);
} finishedCallback:^(NSDictionary *param) {
if (param) {
V2TIMMessage * message = param[@"message"];
NSString * type = param[@"type"];
if ([type isEqualToString:@"image"]) {
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
[strongListener sendMessage:message placeHolderCellData:nil];
}
}
else if ([type isEqualToString:@"video"]) {
TUIMessageCellData *placeHolderCellData = param[@"placeHolderCellData"];
if (placeHolderCellData.msgID.length > 0) {
[TUIChatMediaSendingManager.sharedInstance removeMediaTaskForKey:placeHolderCellData.msgID];
}
BOOL canSendByCurrentPage = NO;
for (id<TUIChatMediaDataListener> currentVC in TUIChatMediaSendingManager.sharedInstance.mediaSendingControllers) {
if ([currentVC.currentConversationID isEqualToString:self.conversationID]&&
[currentVC respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
if (currentVC.isPageAppears) {
[currentVC sendMessage:message placeHolderCellData:placeHolderCellData];
canSendByCurrentPage = YES;
break;
}
}
}
if (!canSendByCurrentPage) {
if ([strongListener respondsToSelector:@selector(sendMessage:placeHolderCellData:)]) {
[strongListener sendMessage:message placeHolderCellData:placeHolderCellData];
}
}
}
else {
// do nothing
}
}
}];
}
else {
//defalut AlbumPicker
[self _selectPhoto];
}
}
- (void)_selectPhoto {
dispatch_async(dispatch_get_main_queue(), ^{
if (@available(iOS 14.0, *)) {
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] init];
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] ]];
configuration.selectionLimit = kTUIChatMediaSelectImageMax;
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:configuration];
picker.delegate = self;
picker.modalPresentationStyle = UIModalPresentationFullScreen;
picker.view.backgroundColor = [UIColor whiteColor];
[self.presentViewController presentViewController:picker animated:YES completion:nil];
} else {
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
picker.delegate = self;
[self.presentViewController presentViewController:picker animated:YES completion:nil];
}
}
});
}
- (void)takePicture {
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
[[MultimediaRecorder sharedInstance].advancedVideoRecorder takePhoneWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
NSData *imageData = [NSData dataWithContentsOfURL:uri];
UIImage *photo = [UIImage imageWithData:imageData];
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
[[NSFileManager defaultManager] createFileAtPath:path
contents:UIImagePNGRepresentation(photo) attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
}];
}
else {
//defalut PhotoCamera
[self _takePicture];
}
}
- (void)_takePicture {
__weak typeof(self) weakSelf = self;
void (^actionBlock)(void) = ^(void) {
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
vc.type = TUICameraMediaTypePhoto;
vc.delegate = weakSelf;
if (weakSelf.presentViewController.navigationController) {
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
} else {
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
}
};
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), ^{
actionBlock();
});
} else {
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
dispatch_async(dispatch_get_main_queue(), ^{
actionBlock();
});
}];
};
}
}
- (void)executeBlockWithMicroAndCameraAuth:(void(^)(void))block{
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization] && [TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
} else {
if (![TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
[TUIUserAuthorizationCenter microStateActionWithPopCompletion:^{
if ([TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
}
}];
}
if (![TUIUserAuthorizationCenter isEnableCameraAuthorization]) {
[TUIUserAuthorizationCenter cameraStateActionWithPopCompletion:^{
if ([TUIUserAuthorizationCenter isEnableMicroAuthorization]) {
dispatch_async(dispatch_get_main_queue(), block);
}
}];
}
}
}
- (void)takeVideo {
if ([MultimediaRecorder sharedInstance].advancedVideoRecorder) {
[[MultimediaRecorder sharedInstance].advancedVideoRecorder recordVideoWithCaller:self.presentViewController successBlock:^(NSURL * _Nonnull uri) {
if (uri) {
if ([uri.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:YES message:nil videoUrl:uri];
return;
}
else if ([self isImageURL:uri]){
NSData *imageData = [NSData dataWithContentsOfURL:uri];
UIImage *photo = [UIImage imageWithData:imageData];
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
[[NSFileManager defaultManager] createFileAtPath:path
contents:UIImagePNGRepresentation(photo) attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
}
else {
[self transcodeIfNeed:YES message:nil videoUrl:uri];
}
}
} failureBlock:^(NSInteger errorCode, NSString * _Nonnull errorMessage) {
}];
return;
}
else {
//defalut VideoRecorder
[self _takeVideo];
}
}
- (void)_takeVideo {
__weak typeof(self) weakSelf = self;
void (^actionBlock)(void) = ^(void) {
TUICameraViewController *vc = [[TUICameraViewController alloc] init];
vc.type = TUICameraMediaTypeVideo;
vc.videoMinimumDuration = 1.5;
vc.delegate = weakSelf;
if ([TUIChatConfig defaultConfig].maxVideoRecordDuration > 0) {
vc.videoMaximumDuration = [TUIChatConfig defaultConfig].maxVideoRecordDuration;
}
if (weakSelf.presentViewController.navigationController) {
[weakSelf.presentViewController.navigationController pushViewController:vc animated:YES];
} else {
[weakSelf.presentViewController presentViewController:vc animated:YES completion:nil];
}
};
[self executeBlockWithMicroAndCameraAuth:actionBlock];
}
- (void)selectFile {
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ (NSString *)kUTTypeData ]
inMode:UIDocumentPickerModeOpen];
picker.delegate = self;
[self.presentViewController presentViewController:picker animated:YES completion:nil];
}
- (BOOL)isImageURL:(NSURL *)url {
NSArray *imageExtensions = @[@"jpg", @"jpeg", @"png", @"gif", @"bmp", @"tiff", @"webp", @"heic"];
NSString *pathExtension = url.pathExtension.lowercaseString;
return [imageExtensions containsObject:pathExtension];
}
#pragma mark - Private Do task
- (void)handleImagePick:(BOOL)succ message:(NSString *)message imageData:(NSData *)imageData {
static NSDictionary *imageFormatExtensionMap = nil;
if (imageFormatExtensionMap == nil) {
imageFormatExtensionMap = @{
@(SDImageFormatUndefined) : @"",
@(SDImageFormatJPEG) : @"jpeg",
@(SDImageFormatPNG) : @"png",
@(SDImageFormatGIF) : @"gif",
@(SDImageFormatTIFF) : @"tiff",
@(SDImageFormatWebP) : @"webp",
@(SDImageFormatHEIC) : @"heic",
@(SDImageFormatHEIF) : @"heif",
@(SDImageFormatPDF) : @"pdf",
@(SDImageFormatSVG) : @"svg",
@(SDImageFormatBMP) : @"bmp",
@(SDImageFormatRAW) : @"raw"
};
}
dispatch_async(dispatch_get_main_queue(), ^{
if (succ == NO || imageData == nil) {
if ([self.listener respondsToSelector:@selector(onProvideImageError:)]) {
[self.listener onProvideImageError:message];
}
return;
}
UIImage *image = [UIImage imageWithData:imageData];
NSData *data = UIImageJPEGRepresentation(image, 1);
NSString *path = [TUIKit_Image_Path stringByAppendingString:[TUITool genImageName:nil]];
NSString *extenionName = [imageFormatExtensionMap objectForKey:@(image.sd_imageFormat)];
if (extenionName.length > 0) {
path = [path stringByAppendingPathExtension:extenionName];
}
int32_t imageFormatSizeMax = 28 * 1024 * 1024;
if (image.sd_imageFormat == SDImageFormatGIF) {
imageFormatSizeMax = 10 * 1024 * 1024;
}
if (imageData.length > imageFormatSizeMax) {
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
[self.listener onProvideFileError:TIMCommonLocalizableString(TUIKitImageSizeCheckLimited)];
}
return;
}
if (image.sd_imageFormat != SDImageFormatGIF) {
UIImage *newImage = image;
UIImageOrientation imageOrientation = image.imageOrientation;
CGFloat aspectRatio = MIN(1920 / image.size.width, 1920 / image.size.height);
CGFloat aspectWidth = image.size.width * aspectRatio;
CGFloat aspectHeight = image.size.height * aspectRatio;
UIGraphicsBeginImageContext(CGSizeMake(aspectWidth, aspectHeight));
[image drawInRect:CGRectMake(0, 0, aspectWidth, aspectHeight)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
data = UIImageJPEGRepresentation(newImage, 0.75);
}
[[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideImage:)]) {
[self.listener onProvideImage:path];
}
});
}
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url {
if (succ == NO || url == nil) {
[self handleVideoPick:NO message:message videoUrl:nil];
return;
}
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:succ message:message videoUrl:url];
return;
}
NSString *tempPath = NSTemporaryDirectory();
NSURL *urlName = [url URLByDeletingPathExtension];
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:newUrl.path]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
if (!success || error) {
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
return;
}
}
// mov to mp4
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
exportSession.outputURL = newUrl;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.shouldOptimizeForNetworkUse = YES;
// intercept FirstTime VideoPicture
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:image Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export session failed");
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
}
break;
default:
break;
}
}];
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (exportSession.status == AVAssetExportSessionStatusExporting) {
NSLog(@"exportSession.progress:%f",exportSession.progress);
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
}
}];
}];
}
else {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl];
} break;
default:
break;
}
}];
}
});
}
- (void)transcodeIfNeed:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)url placeHolderCellData:(TUIMessageCellData*)placeHolderCellData {
if (succ == NO || url == nil) {
[self handleVideoPick:NO message:message videoUrl:nil];
return;
}
if ([url.pathExtension.lowercaseString isEqualToString:@"mp4"]) {
[self handleVideoPick:succ message:message videoUrl:url];
return;
}
NSString *tempPath = NSTemporaryDirectory();
NSURL *urlName = [url URLByDeletingPathExtension];
NSURL *newUrl = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@%@.mp4", tempPath, [urlName.lastPathComponent stringByRemovingPercentEncoding]]];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:newUrl.path]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:newUrl.path error:&error];
if (!success || error) {
NSAssert1(NO, @"removeItemFail: %@", error.localizedDescription);
return;
}
}
// mov to mp4
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetHighestQuality];
exportSession.outputURL = newUrl;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.shouldOptimizeForNetworkUse = YES;
// intercept FirstTime VideoPicture
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export session failed");
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl placeHolderCellData:placeHolderCellData];
}
break;
default:
break;
}
}];
[NSTimer tui_scheduledTimerWithTimeInterval:.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (exportSession.status == AVAssetExportSessionStatusExporting) {
NSLog(@"exportSession.progress:%f",exportSession.progress);
placeHolderCellData.videoTranscodingProgress = exportSession.progress;
}
}];
}
else {
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusCompleted: {
// Video conversion finished
NSLog(@"Successful!");
[self handleVideoPick:succ message:message videoUrl:newUrl];
} break;
default:
break;
}
}];
}
});
}
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl {
[self handleVideoPick:succ message:message videoUrl:videoUrl placeHolderCellData:nil];
}
- (void)handleVideoPick:(BOOL)succ message:(NSString *)message videoUrl:(NSURL *)videoUrl placeHolderCellData:(TUIMessageCellData*)placeHolderCellData{
if (succ == NO || videoUrl == nil) {
if ([self.listener respondsToSelector:@selector(onProvideVideoError:)]) {
[self.listener onProvideVideoError:message];
}
return;
}
NSData *videoData = [NSData dataWithContentsOfURL:videoUrl];
NSString *videoPath = [NSString stringWithFormat:@"%@%@_%u.mp4", TUIKit_Video_Path, [TUITool genVideoName:nil],arc4random()];
[[NSFileManager defaultManager] createFileAtPath:videoPath contents:videoData attributes:nil];
NSDictionary *opts = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:videoUrl options:opts];
NSInteger duration = (NSInteger)urlAsset.duration.value / urlAsset.duration.timescale;
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:urlAsset];
gen.appliesPreferredTrackTransform = YES;
gen.maximumSize = CGSizeMake(192, 192);
NSError *error = nil;
CMTime actualTime;
CMTime time = CMTimeMakeWithSeconds(0.5, 30);
CGImageRef imageRef = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
CGImageRelease(imageRef);
NSData *imageData = UIImagePNGRepresentation(image);
NSString *imagePath = [TUIKit_Video_Path stringByAppendingFormat:@"%@_%u",[TUITool genSnapshotName:nil],arc4random()];
[[NSFileManager defaultManager] createFileAtPath:imagePath contents:imageData attributes:nil];
if ([self.listener respondsToSelector:@selector(onProvideVideo:snapshot:duration:placeHolderCellData:)]) {
[self.listener onProvideVideo:videoPath snapshot:imagePath duration:duration placeHolderCellData:placeHolderCellData];
}
}
#pragma mark - PHPickerViewControllerDelegate
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14)) {
dispatch_async(dispatch_get_main_queue(), ^{
[picker dismissViewControllerAnimated:YES completion:nil];
[[[UIApplication sharedApplication] keyWindow] endEditing:YES];
});
if (!results || results.count == 0) {
return;
}
PHPickerResult *result = [results firstObject];
for (PHPickerResult *result in results) {
[self _dealPHPickerResultFinishPicking:result];
}
}
- (void)_dealPHPickerResultFinishPicking:(PHPickerResult *)result API_AVAILABLE(ios(14)) {
NSItemProvider *itemProvoider = result.itemProvider;
__weak typeof(self) weakSelf = self;
if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeImage
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
BOOL succ = YES;
NSString *message = nil;
if (error) {
succ = NO;
message = error.localizedDescription;
}
[weakSelf handleImagePick:succ message:message imageData:data];
});
}];
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMPEG4]) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *fileName = @"temp.mp4";
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"video not found" videoUrl:newUrl];
});
}];
} else if ([itemProvoider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) {
// Mov type: screen first
if ([self.listener respondsToSelector:@selector(onProvidePlaceholderVideoSnapshot:SnapImage:Completion:)]) {
[self.listener onProvidePlaceholderVideoSnapshot:@"" SnapImage:nil Completion:^(BOOL finished, TUIMessageCellData * _Nonnull placeHolderCellData) {
[itemProvoider loadDataRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie
completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
// Non-mp4 format video, temporarily use mov suffix, will be converted to mp4 format later
NSDate *datenow = [NSDate date];
NSString *timeSp = [NSString stringWithFormat:@"%ld", (long)([datenow timeIntervalSince1970]*1000)];
NSString *fileName = [NSString stringWithFormat:@"%@_temp.mov",timeSp];
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
[weakSelf transcodeIfNeed:flag message:flag ? nil : @"movie not found" videoUrl:newUrl placeHolderCellData:placeHolderCellData];
});
}];
}];
}
} else {
NSString *typeIdentifier = result.itemProvider.registeredTypeIdentifiers.firstObject;
[itemProvoider loadFileRepresentationForTypeIdentifier:typeIdentifier
completionHandler:^(NSURL *_Nullable url, NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *result;
NSData *data = [NSData dataWithContentsOfURL:url];
result = [UIImage imageWithData:data];
/**
* Can't get url when typeIdentifier is public.jepg on emulator:
* There is a separate JEPG transcoding issue that only affects the simulator (63426347), please refer to
* https://developer.apple.com/forums/thread/658135 for more information.
*/
});
}];
}
}
#pragma mark - UIImagePickerController
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info {
__weak typeof(self) weakSelf = self;
picker.delegate = nil;
[picker dismissViewControllerAnimated:YES
completion:^{
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
NSURL *url = nil;
if (@available(iOS 11.0, *)) {
url = [info objectForKey:UIImagePickerControllerImageURL];
} else {
url = [info objectForKey:UIImagePickerControllerReferenceURL];
}
BOOL succ = YES;
NSData *imageData = nil;
NSString *errorMessage = nil;
if (url) {
succ = YES;
imageData = [NSData dataWithContentsOfURL:url];
} else {
succ = NO;
errorMessage = @"image not found";
}
[weakSelf handleImagePick:succ message:errorMessage imageData:imageData];
} else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
if (url) {
[weakSelf transcodeIfNeed:YES message:nil videoUrl:url];
return;
}
/**
* In some cases UIImagePickerControllerMediaURL may be empty, use UIImagePickerControllerPHAsset
*/
PHAsset *asset = nil;
if (@available(iOS 11.0, *)) {
asset = [info objectForKey:UIImagePickerControllerPHAsset];
}
if (asset) {
[self originURLWithAsset:asset
completion:^(BOOL success, NSURL *URL) {
[weakSelf transcodeIfNeed:success
message:success ? nil : @"origin url with asset not found"
videoUrl:URL];
}];
return;
}
/**
* UIImagePickerControllerPHAsset may be empty, and other methods need to be used to obtain the original path of the video
* file
*/
url = [info objectForKey:UIImagePickerControllerReferenceURL];
if (url) {
[weakSelf originURLWithRefrenceURL:url
completion:^(BOOL success, NSURL *URL) {
[weakSelf transcodeIfNeed:success
message:success ? nil : @"origin url with asset not found"
videoUrl:URL];
}];
return;
}
// not support the video
[weakSelf transcodeIfNeed:NO message:@"not support the video" videoUrl:nil];
}
}];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
}
/**
* Get the original file path based on UIImagePickerControllerReferenceURL
*/
- (void)originURLWithRefrenceURL:(NSURL *)URL completion:(void (^)(BOOL success, NSURL *URL))completion {
if (completion == nil) {
return;
}
NSDictionary *queryInfo = [self dictionaryWithURLQuery:URL.query];
NSString *fileName = @"temp.mp4";
if ([queryInfo.allKeys containsObject:@"id"] && [queryInfo.allKeys containsObject:@"ext"]) {
fileName = [NSString stringWithFormat:@"%@.%@", queryInfo[@"id"], [queryInfo[@"ext"] lowercaseString]];
}
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
[assetLibrary assetForURL:URL
resultBlock:^(ALAsset *asset) {
if (asset == nil) {
completion(NO, nil);
return;
}
ALAssetRepresentation *rep = [asset defaultRepresentation];
Byte *buffer = (Byte *)malloc(rep.size);
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; // this is NSData may be what you want
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
completion(flag, newUrl);
}
failureBlock:^(NSError *err) {
completion(NO, nil);
}];
}
- (void)originURLWithAsset:(PHAsset *)asset completion:(void (^)(BOOL success, NSURL *URL))completion {
if (completion == nil) {
return;
}
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
if (resources.count == 0) {
completion(NO, nil);
return;
}
PHAssetResourceRequestOptions *options = [[PHAssetResourceRequestOptions alloc] init];
options.networkAccessAllowed = NO;
__block BOOL invoked = NO;
[PHAssetResourceManager.defaultManager requestDataForAssetResource:resources.firstObject
options:options
dataReceivedHandler:^(NSData *_Nonnull data) {
/**
*
* There will be a problem of repeated callbacks here
*/
if (invoked) {
return;
}
invoked = YES;
if (data == nil) {
completion(NO, nil);
return;
}
NSString *fileName = @"temp.mp4";
NSString *tempPath = NSTemporaryDirectory();
NSString *filePath = [tempPath stringByAppendingPathComponent:fileName];
if ([NSFileManager.defaultManager isDeletableFileAtPath:filePath]) {
[NSFileManager.defaultManager removeItemAtPath:filePath error:nil];
}
NSURL *newUrl = [NSURL fileURLWithPath:filePath];
BOOL flag = [NSFileManager.defaultManager createFileAtPath:filePath contents:data attributes:nil];
completion(flag, newUrl);
}
completionHandler:^(NSError *_Nullable error) {
completion(NO, nil);
}];
}
- (NSDictionary *)dictionaryWithURLQuery:(NSString *)query {
NSArray *components = [query componentsSeparatedByString:@"&"];
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (NSString *item in components) {
NSArray *subs = [item componentsSeparatedByString:@"="];
if (subs.count == 2) {
[dict setObject:subs.lastObject forKey:subs.firstObject];
}
}
return [NSDictionary dictionaryWithDictionary:dict];
;
}
#pragma mark - TUICameraViewControllerDelegate
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithVideoURL:(NSURL *)url {
[self transcodeIfNeed:YES message:nil videoUrl:url];
}
- (void)cameraViewController:(TUICameraViewController *)controller didFinishPickingMediaWithImageData:(NSData *)data {
[self handleImagePick:YES message:nil imageData:data];
}
- (void)cameraViewControllerDidCancel:(TUICameraViewController *)controller {
}
- (void)cameraViewControllerDidPictureLib:(TUICameraViewController *)controller finishCallback:(void (^)(void))callback {
[self selectPhoto];
if (callback) {
callback();
}
}
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {
[url startAccessingSecurityScopedResource];
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
NSError *error;
@weakify(self);
[coordinator
coordinateReadingItemAtURL:url
options:0
error:&error
byAccessor:^(NSURL *newURL) {
@strongify(self);
NSData *fileData = [NSData dataWithContentsOfURL:newURL options:NSDataReadingMappedIfSafe error:nil];
NSString *fileName = [url lastPathComponent];
NSString *filePath = [TUIKit_File_Path stringByAppendingString:fileName];
if (fileData.length > 1e9 || fileData.length == 0) { // 1e9 bytes = 1GB
UIAlertController *ac = [UIAlertController alertControllerWithTitle:TIMCommonLocalizableString(TUIKitFileSizeCheckLimited) message:nil preferredStyle:UIAlertControllerStyleAlert];
[ac tuitheme_addAction:[UIAlertAction actionWithTitle:TIMCommonLocalizableString(Confirm) style:UIAlertActionStyleDefault handler:nil]];
[self.presentViewController presentViewController:ac animated:YES completion:nil];
return;
}
if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
/**
* If a file with the same name exists, increment the file name
*/
int i = 0;
NSArray *arrayM = [NSFileManager.defaultManager subpathsAtPath:TUIKit_File_Path];
for (NSString *sub in arrayM) {
if ([sub.pathExtension isEqualToString:fileName.pathExtension] &&
[sub.stringByDeletingPathExtension tui_containsString:fileName.stringByDeletingPathExtension]) {
i++;
}
}
if (i) {
fileName = [fileName
stringByReplacingOccurrencesOfString:fileName.stringByDeletingPathExtension
withString:[NSString stringWithFormat:@"%@(%d)", fileName.stringByDeletingPathExtension, i]];
filePath = [TUIKit_File_Path stringByAppendingString:fileName];
}
}
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
if ([self.listener respondsToSelector:@selector(onProvideFile:filename:fileSize:)]) {
[self.listener onProvideFile:filePath filename:fileName fileSize:fileSize];
}
} else {
if ([self.listener respondsToSelector:@selector(onProvideFileError:)]) {
[self.listener onProvideFileError:@"file not found"];
}
}
}];
[url stopAccessingSecurityScopedResource];
[controller dismissViewControllerAnimated:YES completion:nil];
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
[controller dismissViewControllerAnimated:YES completion:nil];
}
@end

View File

@@ -0,0 +1,14 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
@import UIKit;
@interface TUIMotionManager : NSObject
@property(nonatomic, assign) UIDeviceOrientation deviceOrientation;
@property(nonatomic, assign) AVCaptureVideoOrientation videoOrientation;
@end

View File

@@ -0,0 +1,61 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMotionManager.h"
#import <CoreMotion/CoreMotion.h>
@import UIKit;
@interface TUIMotionManager ()
@property(nonatomic, strong) CMMotionManager *motionManager;
@end
@implementation TUIMotionManager
- (instancetype)init {
self = [super init];
if (self) {
_motionManager = [[CMMotionManager alloc] init];
_motionManager.deviceMotionUpdateInterval = 1 / 15.0;
if (!_motionManager.deviceMotionAvailable) {
_motionManager = nil;
return self;
}
__weak __typeof(self) weakSelf = self;
[_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion *motion, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf performSelectorOnMainThread:@selector(handleDeviceMotion:) withObject:motion waitUntilDone:YES];
}];
}
return self;
}
- (void)handleDeviceMotion:(CMDeviceMotion *)deviceMotion {
double x = deviceMotion.gravity.x;
double y = deviceMotion.gravity.y;
if (fabs(y) >= fabs(x)) {
if (y >= 0) {
_deviceOrientation = UIDeviceOrientationPortraitUpsideDown;
_videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
} else {
_deviceOrientation = UIDeviceOrientationPortrait;
_videoOrientation = AVCaptureVideoOrientationPortrait;
}
} else {
if (x >= 0) {
_deviceOrientation = UIDeviceOrientationLandscapeRight;
_videoOrientation = AVCaptureVideoOrientationLandscapeRight;
} else {
_deviceOrientation = UIDeviceOrientationLandscapeLeft;
_videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
}
}
}
- (void)dealloc {
[_motionManager stopDeviceMotionUpdates];
}
@end

View File

@@ -0,0 +1,26 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TUIMovieManager : NSObject
@property(nonatomic, assign) AVCaptureVideoOrientation referenceOrientation;
@property(nonatomic, assign) AVCaptureVideoOrientation currentOrientation;
@property(nonatomic, strong) AVCaptureDevice *currentDevice;
- (void)start:(void (^)(NSError *error))handle;
- (void)stop:(void (^)(NSURL *url, NSError *error))handle;
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,235 @@
// Created by Tencent on 2023/06/09.
// Copyright © 2023 Tencent. All rights reserved.
#import "TUIMovieManager.h"
#import <TIMCommon/TIMDefine.h>
@interface TUIMovieManager () {
BOOL _readyToRecordVideo;
BOOL _readyToRecordAudio;
dispatch_queue_t _movieWritingQueue;
NSURL *_movieURL;
AVAssetWriter *_movieWriter;
AVAssetWriterInput *_movieAudioInput;
AVAssetWriterInput *_movieVideoInput;
}
@end
@implementation TUIMovieManager
- (instancetype)init {
self = [super init];
if (self) {
_movieWritingQueue = dispatch_queue_create("com.tui.Movie.Writing.Queue", DISPATCH_QUEUE_SERIAL);
_movieURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"TUICaptureTempMovie.mp4"]];
_referenceOrientation = AVCaptureVideoOrientationPortrait;
}
return self;
}
- (void)start:(void (^)(NSError *error))handle {
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
[self removeFile:self->_movieURL];
NSError *error;
if (!self->_movieWriter) {
self->_movieWriter = [[AVAssetWriter alloc] initWithURL:self->_movieURL fileType:AVFileTypeMPEG4 error:&error];
}
handle(error);
});
}
- (void)stop:(void (^)(NSURL *url, NSError *error))handle {
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
self->_readyToRecordVideo = NO;
self->_readyToRecordAudio = NO;
if (self->_movieWriter && self->_movieWriter.status == AVAssetWriterStatusWriting) {
@weakify(self);
[self->_movieWriter finishWritingWithCompletionHandler:^() {
@strongify(self);
@weakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self);
if (self->_movieWriter.status == AVAssetWriterStatusCompleted) {
handle(self->_movieURL, nil);
} else {
handle(nil, self->_movieWriter.error);
}
self->_movieWriter = nil;
});
}];
} else {
[self->_movieWriter cancelWriting];
self->_movieWriter = nil;
dispatch_async(dispatch_get_main_queue(), ^{
handle(nil, [NSError errorWithDomain:@"com.tui.Movie.Writing" code:0 userInfo:@{NSLocalizedDescriptionKey : @"AVAssetWriter status error"}]);
});
}
});
}
- (void)writeData:(AVCaptureConnection *)connection video:(AVCaptureConnection *)video audio:(AVCaptureConnection *)audio buffer:(CMSampleBufferRef)buffer {
CFRetain(buffer);
@weakify(self);
dispatch_async(_movieWritingQueue, ^{
@strongify(self);
if (connection == video) {
if (!self->_readyToRecordVideo) {
self->_readyToRecordVideo = [self setupAssetWriterVideoInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
}
if ([self inputsReadyToRecord]) {
[self writeSampleBuffer:buffer ofType:AVMediaTypeVideo];
}
} else if (connection == audio) {
if (!self->_readyToRecordAudio) {
self->_readyToRecordAudio = [self setupAssetWriterAudioInput:CMSampleBufferGetFormatDescription(buffer)] == nil;
}
if ([self inputsReadyToRecord]) {
[self writeSampleBuffer:buffer ofType:AVMediaTypeAudio];
}
}
CFRelease(buffer);
});
}
- (void)writeSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(NSString *)mediaType {
if (_movieWriter.status == AVAssetWriterStatusUnknown) {
if ([_movieWriter startWriting]) {
[_movieWriter startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
} else {
NSLog(@"%@", _movieWriter.error);
}
}
if (_movieWriter.status == AVAssetWriterStatusWriting) {
if (mediaType == AVMediaTypeVideo) {
if (!_movieVideoInput.isReadyForMoreMediaData) {
return;
}
if (![_movieVideoInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"%@", _movieWriter.error);
}
} else if (mediaType == AVMediaTypeAudio) {
if (!_movieAudioInput.isReadyForMoreMediaData) {
return;
}
if (![_movieAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"%@", _movieWriter.error);
}
}
}
}
- (BOOL)inputsReadyToRecord {
return _readyToRecordVideo && _readyToRecordAudio;
}
- (NSError *)setupAssetWriterAudioInput:(CMFormatDescriptionRef)currentFormatDescription {
size_t aclSize = 0;
const AudioStreamBasicDescription *currentASBD = CMAudioFormatDescriptionGetStreamBasicDescription(currentFormatDescription);
const AudioChannelLayout *channelLayout = CMAudioFormatDescriptionGetChannelLayout(currentFormatDescription, &aclSize);
NSData *dataLayout = aclSize > 0 ? [NSData dataWithBytes:channelLayout length:aclSize] : [NSData data];
NSDictionary *settings = @{
AVFormatIDKey : [NSNumber numberWithInteger:kAudioFormatMPEG4AAC],
AVSampleRateKey : [NSNumber numberWithFloat:currentASBD->mSampleRate],
AVChannelLayoutKey : dataLayout,
AVNumberOfChannelsKey : [NSNumber numberWithInteger:currentASBD->mChannelsPerFrame],
AVEncoderBitRatePerChannelKey : [NSNumber numberWithInt:64000]
};
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeAudio]) {
_movieAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:settings];
_movieAudioInput.expectsMediaDataInRealTime = YES;
if ([_movieWriter canAddInput:_movieAudioInput]) {
[_movieWriter addInput:_movieAudioInput];
} else {
return _movieWriter.error;
}
} else {
return _movieWriter.error;
}
return nil;
}
- (NSError *)setupAssetWriterVideoInput:(CMFormatDescriptionRef)currentFormatDescription {
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(currentFormatDescription);
NSUInteger numPixels = dimensions.width * dimensions.height;
CGFloat bitsPerPixel = numPixels < (640 * 480) ? 4.05 : 11.0;
NSDictionary *compression =
@{AVVideoAverageBitRateKey : [NSNumber numberWithInteger:numPixels * bitsPerPixel], AVVideoMaxKeyFrameIntervalKey : [NSNumber numberWithInteger:30]};
NSDictionary *settings = @{
AVVideoCodecKey : AVVideoCodecH264,
AVVideoWidthKey : [NSNumber numberWithInteger:dimensions.width],
AVVideoHeightKey : [NSNumber numberWithInteger:dimensions.height],
AVVideoCompressionPropertiesKey : compression
};
if ([_movieWriter canApplyOutputSettings:settings forMediaType:AVMediaTypeVideo]) {
_movieVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:settings];
_movieVideoInput.expectsMediaDataInRealTime = YES;
_movieVideoInput.transform = [self transformFromCurrentVideoOrientationToOrientation:self.referenceOrientation];
if ([_movieWriter canAddInput:_movieVideoInput]) {
[_movieWriter addInput:_movieVideoInput];
} else {
return _movieWriter.error;
}
} else {
return _movieWriter.error;
}
return nil;
}
- (CGAffineTransform)transformFromCurrentVideoOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
CGFloat orientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:orientation];
CGFloat videoOrientationAngleOffset = [self angleOffsetFromPortraitOrientationToOrientation:self.currentOrientation];
CGFloat angleOffset;
if (self.currentDevice.position == AVCaptureDevicePositionBack) {
angleOffset = videoOrientationAngleOffset - orientationAngleOffset + M_PI_2;
} else {
angleOffset = orientationAngleOffset - videoOrientationAngleOffset + M_PI_2;
}
CGAffineTransform transform = CGAffineTransformMakeRotation(angleOffset);
return transform;
}
- (CGFloat)angleOffsetFromPortraitOrientationToOrientation:(AVCaptureVideoOrientation)orientation {
CGFloat angle = 0.0;
switch (orientation) {
case AVCaptureVideoOrientationPortrait:
angle = 0.0;
break;
case AVCaptureVideoOrientationPortraitUpsideDown:
angle = M_PI;
break;
case AVCaptureVideoOrientationLandscapeRight:
angle = -M_PI_2;
break;
case AVCaptureVideoOrientationLandscapeLeft:
angle = M_PI_2;
break;
}
return angle;
}
- (void)removeFile:(NSURL *)fileURL {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *filePath = fileURL.path;
if ([fileManager fileExistsAtPath:filePath]) {
NSError *error;
BOOL success = [fileManager removeItemAtPath:filePath error:&error];
if (!success) {
NSAssert(NO, error.localizedDescription);
NSLog(@"Failed to delete file%@", error);
} else {
NSLog(@"Succeed to delete file");
}
}
}
@end