Files
featherVoice/TUIKit/TUIChat/CommonUI/Camera/TUICameraViewController.m
2025-08-08 10:49:36 +08:00

414 lines
16 KiB
Objective-C

// 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