iOS 屏幕录制实现

iOS 屏幕录制实现

录屏API版本变化

  • 主要使用iOS系统的Airplay功能和ReplayKit库实现屏幕录制
  • iOS9开始,苹果新增了 ReplayKit 框架,使用该框架中的API进行录屏,该功能只能录制应用内屏幕,且无法操作视频/音频流,最终只能在预览页面进行“保存”、“拷贝”、“分享”等操作。
  • 从iOS 10开始,苹果新增了录制系统屏幕的API,即应用即使退出前台也能持续录制,以下称为“系统屏幕录制”,区分于“应用屏幕录制”。
  • iOS 11官方开放了应用内录屏的流数据处理API,即可直接操作视频流、音频流,而不是只能预览、保存、分享。
  • 对于录制系统内容,iOS11不允许开发直接调用api来启动系统界别的录制,必须是用户通过手动启动.用户点击进入手机设置页面-> 控制中心-> 自定义 , 找到屏幕录制的功能按钮,将其添加到上方:添加成功
  • 在iOS 12.0+上出现了一个新的UI控件RPSystemBroadcastPickerView,用于展示用户启动系统录屏的指定视图.可以在App界面手动出发录屏

App内部录制屏幕

  • 从App内部录制屏幕,不支持系统界面。只能录制App。
  • 关键类 RPScreenRecorder

录音麦克风声音

  • 首先开启麦克风权限,添加相关配置plist
//
//  ViewController
//
//
//  Created by song on 2022/01/13.
//  Copyright © 2022 song. All rights reserved.

#import "MainViewController.h"
#import <ReplayKit/ReplayKit.h>
#import <AVFoundation/AVFoundation.h>
#import "SystemScreenRecordController.h"

@interface MainViewController ()<RPScreenRecorderDelegate,RPPreviewViewControllerDelegate>
@end

@implementation MainViewController

-(void)viewDidLoad{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self setupUI];
    [self setupScreen];
}
- (void)setupScreen{
    AVAuthorizationStatus microPhoneStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
      switch (microPhoneStatus) {
          case AVAuthorizationStatusDenied:
          case AVAuthorizationStatusRestricted:
          {
              // 被拒绝
              [self goMicroPhoneSet];
          }
              break;
          case AVAuthorizationStatusNotDetermined:
          {
              // 没弹窗
              [self requestMicroPhoneAuth];
          }
              break;
          case AVAuthorizationStatusAuthorized:
          {
              // 有授权
          }
              break;

          default:
              break;
      }
    
}
-(void) goMicroPhoneSet
{
    UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"您还没有允许麦克风权限" message:@"去设置一下吧" preferredStyle:UIAlertControllerStyleAlert];

    UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

    }];
    UIAlertAction * setAction = [UIAlertAction actionWithTitle:@"去设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSURL * url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
            [UIApplication.sharedApplication openURL:url options:nil completionHandler:^(BOOL success) {

            }];
        });
    }];

    [alert addAction:cancelAction];
    [alert addAction:setAction];

    [self presentViewController:alert animated:YES completion:nil];
}
-(void) requestMicroPhoneAuth
{
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {

    }];
}
- (void)setupUI{
    self.title= @"录屏Demo";
    self.navigationController.navigationBar.tintColor=[UIColor whiteColor];
    self.navigationController.navigationBar.barTintColor = [UIColor greenColor];
    self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
    [self.navigationController.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName:[UIColor whiteColor],NSFontAttributeName:[UIFont systemFontOfSize:25]}];
    
    UIBarButtonItem *leftBar = [[UIBarButtonItem alloc ] initWithTitle:@"开始录屏" style:UIBarButtonItemStylePlain target:self action:@selector(start)];
    UIBarButtonItem *playBtn = [[UIBarButtonItem alloc] initWithTitle:@"结束录屏" style:UIBarButtonItemStylePlain target:self action:@selector(stop)];
    
    self.navigationItem.rightBarButtonItem = playBtn;
    
    self.navigationItem.leftBarButtonItem = leftBar;
    
    UIButton *btn1 =  [UIButton buttonWithType:UIButtonTypeSystem];
    btn1.frame = CGRectMake(110, 100, 100, 33);
    btn1.backgroundColor = [UIColor redColor];
    [btn1 setTitle:@"点我啊" forState:UIControlStateNormal];
    [btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn1];
}
- (void)systemBtnClick{
    SystemScreenRecordController *vc = [[SystemScreenRecordController alloc] init];
    vc.hidesBottomBarWhenPushed = YES;
    [self.navigationController pushViewController:vc animated:YES];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath:%@,change:%@",keyPath,change);
    if ([keyPath isEqualToString:@"available"] && [change[@"new"] integerValue] == 1) {
        [self start];
    }
}
- (void)checkout{
    
    if (@available(iOS 9.0, *)) {
        if ([RPScreenRecorder sharedRecorder].available) {
            NSLog(@"可以录屏");
            [self start];
            
        }else{
            NSLog(@"未授权");
            [[RPScreenRecorder sharedRecorder] addObserver:self forKeyPath:@"available" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
        }
    } else {
        NSLog(@"不支持录屏");
    }

}
- (void)start{
    if ([RPScreenRecorder sharedRecorder].recording) {
        NSLog(@"录制中...");
    }else{
        NSLog(@"1---[RPScreenRecorder sharedRecorder].microphoneEnabled:%d",[RPScreenRecorder sharedRecorder].microphoneEnabled);
        if(![RPScreenRecorder sharedRecorder].microphoneEnabled){
            [[RPScreenRecorder sharedRecorder] setMicrophoneEnabled:YES];
        }
        NSLog(@"2---[RPScreenRecorder sharedRecorder].microphoneEnabled:%d",[RPScreenRecorder sharedRecorder].microphoneEnabled);
        [RPScreenRecorder sharedRecorder].delegate = self;
        if (@available(iOS 11.0, *)) {
            [[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef  _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
                NSLog(@"拿到流,可以直播推流");
                switch (bufferType) {
                    case RPSampleBufferTypeAudioApp:
                        NSLog(@"内部音频流");
                        break;
                    case RPSampleBufferTypeVideo:
                        NSLog(@"内部视频流");
                        break;
                    case RPSampleBufferTypeAudioMic:
                        NSLog(@"麦克风音频");
                        break;
                    default:
                        break;
                }
            } completionHandler:^(NSError * _Nullable error) {
                NSLog(@"startCaptureWithHandler completionHandler");
                if (error) {
                    
                }else{
                    
                }
            }];
        }
        else if (@available(iOS 10.0, *)) {
            [[RPScreenRecorder sharedRecorder] startRecordingWithHandler:^(NSError * _Nullable error) {
                NSLog(@"startRecordingWithHandler:%@",error);
            }];
        } else if(@available(iOS 9.0, *))  {
            [[RPScreenRecorder sharedRecorder] startRecordingWithMicrophoneEnabled:YES handler:^(NSError * _Nullable error) {
                NSLog(@"startRecordingWithMicrophoneEnabled:%@",error);
            }];
        }
    
    }
    
    
}
- (void)stop{
    if ([RPScreenRecorder sharedRecorder].recording) {
        [[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
            NSLog(@"stopRecordingWithHandler");
            if (!error) {
                previewViewController.previewControllerDelegate = self;
                [self presentViewController:previewViewController animated:YES completion:nil];
            }
        }];
    }
}

#pragma mark - RPScreenRecorderDelegate
- (void)screenRecorder:(RPScreenRecorder *)screenRecorder didStopRecordingWithPreviewViewController:(RPPreviewViewController *)previewViewController error:(NSError *)error /*API_AVAILABLE(ios(11.0)*/{
    
    if(@available(iOS 11.0,*)){
        NSLog(@"didStopRecordingWithPreviewViewController: %@",error);
    }
}

-(void)screenRecorderDidChangeAvailability:(RPScreenRecorder *)screenRecorder{
    NSLog(@"screenRecorderDidChangeAvailability:%@",screenRecorder);
}

- (void)screenRecorder:(RPScreenRecorder *)screenRecorder didStopRecordingWithError:(NSError *)error previewViewController:(RPPreviewViewController *)previewViewController{
    if(@available(iOS 9.0,*)){
        NSLog(@"didStopRecordingWithError :%@",error);
    }
}


#pragma mark - RPPreviewViewControllerDelegate
- (void)previewControllerDidFinish:(RPPreviewViewController *)previewController{
    NSLog(@"previewControllerDidFinish");
    [previewController dismissViewControllerAnimated:YES completion:nil];

    
}
- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet<NSString *> *)activityTypes{
    NSLog(@"didFinishWithActivityTypes:%@",activityTypes);
}
@end

App内部录屏直播

Bonjour

  • Bonjour 是 Apple 基于标准的网络技术,旨在帮助设备和服务在同一网络上发现彼此。例如,iPhone 和 iPad 设备使用 Bonjour 发现兼容“隔空打印”的打印机,iPhone 和 iPad 设备以及 Mac 电脑使用 Bonjour 发现兼容“隔空播放”的设备(如 Apple TV).

  • Bonjour

  • 由于bonjour服务是开源的,且iOS系统提供底层API库:DNS-SD,去实现此功能。

  • Bonjour服务一般用于发布服务全局广播,但如果服务不想被其它机器知道,只有制定机器知道,如何实现:

    • 1、客户端与服务器通信,等到服务器的服务ip地址,端口号
    • 2、客户端本地创建服务结点,并连接
  • 参考

  • 参考

APP广播端实现

- 被录制端需要在原有功能的基础上,增加一个唤起广播的入口。
- 点击直播会出现直播App选择(实现了ReplayKit Live的APP)
- ![](https://tva1.sinaimg.cn/large/008i3skNgy1gs7adt8fqij30u01szkjl.jpg)
//
//  SystemScreenRecordController.m
//  SLQDemo
//
//  Created by song on 2022/01/6.
//  Copyright © 2022 了. All rights reserved.
//

#import "SystemScreenRecordController.h"
#import <ReplayKit/ReplayKit.h>

@interface SystemScreenRecordController ()<RPBroadcastActivityViewControllerDelegate,RPBroadcastControllerDelegate>

@end

@implementation SystemScreenRecordController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
    UIButton *btn1 =  [UIButton buttonWithType:UIButtonTypeSystem];
    btn1.frame = CGRectMake(110, 100, 100, 33);
    btn1.backgroundColor = [UIColor redColor];
    [btn1 setTitle:@"点我啊" forState:UIControlStateNormal];
    [btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn1];

}
- (void)systemBtnClick {
    [self setupUI];
}

- (void)setupUI {
    
    if (@available(iOS 10.0, *)) {
        [RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {
            if (error) {
                NSLog(@"loadBroadcastActivityViewControllerWithHandler:%@",error);
            }else{
                broadcastActivityViewController.delegate = self;
                broadcastActivityViewController.modalPresentationStyle = UIModalPresentationPopover;
                [self presentViewController:broadcastActivityViewController animated:YES completion:nil];
            }
        }];
    } else {
        NSLog(@"不支持录制系统屏幕");
    }
 
}
#pragma mark - RPBroadcastActivityViewControllerDelegate
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(RPBroadcastController *)broadcastController error:(NSError *)error{
    NSLog(@"broadcastActivityViewController: didFinishWithBroadcastController:");

    dispatch_async(dispatch_get_main_queue(), ^{
        [broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
    });
    
    NSLog(@"Boundle id :%@",broadcastController.broadcastURL);
    
    if (error) {
        NSLog(@"BAC: %@ didFinishWBC: %@, err: %@",
                   broadcastActivityViewController,
                   broadcastController,
                   error);
             return;
    }
    [broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"startBroadcastWithHandler:%@",error);
        }else{
            NSLog(@"startBroadcast success");
        }
    }];
}

- (void)broadcastController:(RPBroadcastController *)broadcastController didUpdateServiceInfo:(NSDictionary<NSString *,NSObject<NSCoding> *> *)serviceInfo{
    NSLog(@"didUpdateServiceInfo:%@",serviceInfo);
}

@end


广播端App(直播平台)的实现

  • 新增对 ReplayKit Live 的支持,只需要创建两个扩展的 target,分别是 Broadcast UI Extension 和 Broadcast Upload Extension
//
//  SampleHandler.m
//  broadcast
//
//  Created by song on 2022/01/6.
//  Copyright © 2022 了. All rights reserved.
//


#import "SampleHandler.h"

@implementation SampleHandler

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
    NSLog(@"启动广播");
    
}

- (void)broadcastPaused {
    // User has requested to pause the broadcast. Samples will stop being delivered.
    NSLog(@"暂停广播");
}

- (void)broadcastResumed {
    // User has requested to resume the broadcast. Samples delivery will resume.
    NSLog(@"恢复广播");
}

- (void)broadcastFinished {
    // User has requested to finish the broadcast.
    NSLog(@"完成广播");
}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            // 得到YUV数据
            NSLog(@"视频流");
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            // 处理app音频
            NSLog(@"App音频流");
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            // 处理麦克风音频
            NSLog(@"麦克风音频流");
            break;
            
        default:
            break;
    }
}

@end

  • 实现录屏信息的界面,可以设置一下标题什么的
//
//  BroadcastSetupViewController.m
//  broadcastSetupUI
//
//  Created by song on 2022/01/07.
//  Copyright © 2022 了. All rights reserved.
//

#import "BroadcastSetupViewController.h"

@implementation BroadcastSetupViewController

- (void)viewDidLoad{
    [super viewDidLoad];
    NSLog(@"BroadcastSetupViewController");
    self.view.backgroundColor = [UIColor redColor];
    UIButton *btn1 =  [UIButton buttonWithType:UIButtonTypeSystem];
    btn1.frame = CGRectMake(110, 100, 200, 33);
    btn1.backgroundColor = [UIColor redColor];
    [btn1 setTitle:@"点我开始直播" forState:UIControlStateNormal];
    [btn1 addTarget:self action:@selector(systemBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn1];
    
    UIButton *btn2 =  [UIButton buttonWithType:UIButtonTypeSystem];
    btn2.frame = CGRectMake(110, 200, 200, 33);
    btn2.backgroundColor = [UIColor redColor];
    [btn2 setTitle:@"取消直播" forState:UIControlStateNormal];
    [btn2 addTarget:self action:@selector(stop) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn2];

}
- (void)systemBtnClick {
    NSLog(@"开始直播");
    [self userDidFinishSetup];
}
- (void)stop {
    [self userDidCancelSetup];
}
// Call this method when the user has finished interacting with the view controller and a broadcast stream can start
- (void)userDidFinishSetup {
    NSLog(@"userDidFinishSetup");
    // URL of the resource where broadcast can be viewed that will be returned to the application
    NSURL *broadcastURL = [NSURL URLWithString:@"http://apple.com/broadcast/test1"];
    
    // Dictionary with setup information that will be provided to broadcast extension when broadcast is started
    NSDictionary *setupInfo = @{ @"broadcastName" : @"App live" };
    
    // Tell ReplayKit that the extension is finished setting up and can begin broadcasting
    [self.extensionContext completeRequestWithBroadcastURL:broadcastURL setupInfo:setupInfo];
}

- (void)userDidCancelSetup {
    // Tell ReplayKit that the extension was cancelled by the user
    NSLog(@"userDidCancelSetup");
    [self.extensionContext cancelRequestWithError:[NSError errorWithDomain:@"YourAppDomain" code:-1 userInfo:nil]];
}

@end

  • 注意
iOS10只支持app内容录制,所以当app切到后台,录制内容将停止;
手机锁屏时,录制进程将停止;
这几个方法中的代码不能阻塞(例如写文件等慢操作),否则导致录制进程停止;

iOS12可在app里手动触发录屏

  • 在iOS 12.0+上出现了一个新的UI控件RPSystemBroadcastPickerView,用于展示用户启动系统录屏的指定视图.
  if (@available(iOS 12.0, *)) {
        self.broadPickerView = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(110, 100, 100, 100)];
        self.broadPickerView.preferredExtension = @"com.ask.answer.live.boradcastr";// nil的话列出所有可录屏的App
        [self.view addSubview:self.broadPickerView];
    }
  • 添加以上代码后,就会多出一个黑色按钮,点击就会弹出录制界面

录屏文件数据的共享

  • 每个Extension都需要一个宿主App,并且有自己的沙盒,当我们把录屏文件保存到沙盒中时宿主App是无法获取到的,那么只有采用共享的方式才能让宿主App拿到录屏文件。
  • App Group Share帮我们解决了这个问题,通过设置组间共享的模式,使得同一个Group下面的App可以共享资源,解决了沙盒的限制。

iOS14

  • 新增录制视频保存之URL的API,可直接保存到相册,保存到沙盒等
- (void)saveVideoWithUrl:(NSURL *)url {
    PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
    [photoLibrary performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];
        
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        if (success) {
            NSLog(@"已将视频保存至相册");
        } else {
            NSLog(@"未能保存视频到相册");
        }
    }];
}
- (void)stop{
    if ([RPScreenRecorder sharedRecorder].recording) {
        
        if (@available(iOS 14.0, *)) {
            __weak typeof(self) weakSelf = self;
            NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) firstObject];
            NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/test.mp4",cachesDir]];
            [[RPScreenRecorder sharedRecorder] stopRecordingWithOutputURL:url  completionHandler:^(NSError * _Nullable error) {
                NSLog(@"stopRecordingWithOutputURL:%@",url);
                [weakSelf saveVideoWithUrl:url];
               
            }];
        } else {
            [[RPScreenRecorder sharedRecorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {
                NSLog(@"stopRecordingWithHandler");
                if (!error) {
                    previewViewController.previewControllerDelegate = self;
                    [self presentViewController:previewViewController animated:YES completion:nil];
                }
            }];
        }
  
    }
}

保存视频到相册

  • 预览视频可通过AVPlayerViewController预览视频

  • 也可以直接保存到相册

  • SampleHandler数据流回调里处理视频

  • 通过AppGroup和宿主app共享数据

//
//  SampleHandler.m
//  broadcast
//
//  Created by song on 2022/01/6.
//  Copyright © 2022 了. All rights reserved.
//


#import "SampleHandler.h"
#import <AVFoundation/AVFoundation.h>

@interface NSDate (Timestamp)
+ (NSString *)timestamp;
@end
 
@implementation NSDate (Timestamp)
+ (NSString *)timestamp {
    long long timeinterval = (long long)([NSDate timeIntervalSinceReferenceDate] * 1000);
    return [NSString stringWithFormat:@"%lld", timeinterval];
}
@end

@interface SampleHandler()
@property (nonatomic,strong) AVAssetWriter *assetWriter;
@property (nonatomic,strong) AVAssetWriterInput *videoInput;
@property (nonatomic,strong) AVAssetWriterInput *audioInput;
@end

@implementation SampleHandler

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
    NSLog(@"启动广播:%@",setupInfo);
    [self initData];
}

- (NSString *)getDocumentPath {
    
    static NSString *replaysPath;
    if (!replaysPath) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *documentRootPath = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.ask.answer.live"];
        replaysPath = [documentRootPath.path stringByAppendingPathComponent:@"Replays"];
        if (![fileManager fileExistsAtPath:replaysPath]) {
            NSError *error_createPath = nil;
            BOOL success_createPath = [fileManager createDirectoryAtPath:replaysPath withIntermediateDirectories:true attributes:@{} error:&error_createPath];
            if (success_createPath && !error_createPath) {
                NSLog(@"%@路径创建成功!", replaysPath);
            } else {
                NSLog(@"%@路径创建失败:%@", replaysPath, error_createPath);
            }
        }else{
            NSLog(@"%@路径已存在!", replaysPath);
        }
    }
    return replaysPath;
}
- (NSURL *)getFilePathUrl {
    NSString *time = [NSDate timestamp];
    NSString *fileName = [time stringByAppendingPathExtension:@"mp4"];
    NSString *fullPath = [[self getDocumentPath] stringByAppendingPathComponent:fileName];
    return [NSURL fileURLWithPath:fullPath];
}

- (NSArray <NSURL *> *)fetechAllResource {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    NSString *documentPath = [self getDocumentPath];
    NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
    NSError *error = nil;
    NSArray<NSURL *> *allResource  =  [fileManager contentsOfDirectoryAtURL:documentURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants) error:&error];
    return allResource;
    
}
- (void)initData {
    if ([self.assetWriter canAddInput:self.videoInput]) {
        [self.assetWriter addInput:self.videoInput];
    }else{
        NSLog(@"添加input失败");
    }
}
- (AVAssetWriter *)assetWriter{
    if (!_assetWriter) {
        NSError *error = nil;
        _assetWriter = [[AVAssetWriter alloc] initWithURL:[self getFilePathUrl] fileType:(AVFileTypeMPEG4) error:&error];
        NSAssert(!error, @"_assetWriter 初始化失败");
    }
    return _assetWriter;
}
-(AVAssetWriterInput *)audioInput{
    if (!_audioInput) {
        // 音频参数
        NSDictionary *audioCompressionSettings = @{
            AVEncoderBitRatePerChannelKey:@(28000),
            AVFormatIDKey:@(kAudioFormatMPEG4AAC),
            AVNumberOfChannelsKey:@(1),
            AVSampleRateKey:@(22050)
        };
        _audioInput  = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioCompressionSettings];
    }
    return _audioInput;
}

-(AVAssetWriterInput *)videoInput{
    if (!_videoInput) {
        
        CGSize size = [UIScreen mainScreen].bounds.size;
        // 视频大小
        NSInteger numPixels = size.width * size.height;
        // 像素比
        CGFloat bitsPerPixel = 7.5;
        NSInteger bitsPerSecond = numPixels * bitsPerPixel;
        // 码率和帧率设置
        NSDictionary *videoCompressionSettings = @{
            AVVideoAverageBitRateKey:@(bitsPerSecond),//码率
            AVVideoExpectedSourceFrameRateKey:@(25),// 帧率
            AVVideoMaxKeyFrameIntervalKey:@(15),// 关键帧最大间隔
            AVVideoProfileLevelKey:AVVideoProfileLevelH264BaselineAutoLevel,
            AVVideoPixelAspectRatioKey:@{
                    AVVideoPixelAspectRatioVerticalSpacingKey:@(1),
                    AVVideoPixelAspectRatioHorizontalSpacingKey:@(1)
            }
        };
        CGFloat scale = [UIScreen mainScreen].scale;
        
        // 视频参数
        NSDictionary *videoOutputSettings = @{
            AVVideoCodecKey:AVVideoCodecTypeH264,
            AVVideoScalingModeKey:AVVideoScalingModeResizeAspectFill,
            AVVideoWidthKey:@(size.width*scale),
            AVVideoHeightKey:@(size.height*scale),
            AVVideoCompressionPropertiesKey:videoCompressionSettings
        };
        
        _videoInput  = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoOutputSettings];
        _videoInput.expectsMediaDataInRealTime = true;
    }
    return _videoInput;
}


- (void)broadcastPaused {
    // User has requested to pause the broadcast. Samples will stop being delivered.
    NSLog(@"暂停广播");
    [self stopRecording];
}

- (void)broadcastResumed {
    // User has requested to resume the broadcast. Samples delivery will resume.
    NSLog(@"恢复广播");
    [self stopRecording];
}

- (void)broadcastFinished {
    // User has requested to finish the broadcast.
    NSLog(@"完成广播");
    [self stopRecording];
}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            // 得到YUV数据
            NSLog(@"视频流");
            AVAssetWriterStatus status = self.assetWriter.status;
            if (status == AVAssetWriterStatusFailed || status == AVAssetWriterStatusCompleted || status == AVAssetWriterStatusCancelled) {
                return;
            }
            if (status == AVAssetWriterStatusUnknown) {
                [self.assetWriter startWriting];
                CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
                [self.assetWriter startSessionAtSourceTime:time];
                
            }
            if (status == AVAssetWriterStatusWriting ) {
                if (self.videoInput.isReadyForMoreMediaData) {
                    BOOL success = [self.videoInput appendSampleBuffer:sampleBuffer];
                    if (!success) {
                        [self stopRecording];
                    }
                }
            }
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            // 处理app音频
            NSLog(@"App音频流");
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            // 处理麦克风音频
            NSLog(@"麦克风音频流");
            if (self.audioInput.isReadyForMoreMediaData) {
                BOOL success = [self.audioInput appendSampleBuffer:sampleBuffer];
                if (!success) {
                    [self stopRecording];
                }
            }
            break;
            
        default:
            break;
    }
}
- (void)stopRecording {
//    if (self.assetWriter.status == AVAssetWriterStatusWriting) {

        [self.assetWriter finishWritingWithCompletionHandler:^{
            NSLog(@"结束写入数据");
        }];
//        [self.audioInput markAsFinished];
//    }
}

@end

  • 预览视频
- (void)watchRecord:(UIButton *)sender {
    NSLog(@"watchRecord");
    NSArray<NSURL *> *allResource = [[self fetechAllResource] sortedArrayUsingComparator:^NSComparisonResult(NSURL *  _Nonnull obj1, NSURL * _Nonnull obj2) {
        //排序,每次都查看最新录制的视频
        return [obj2.path compare:obj1.path options:(NSCaseInsensitiveSearch)];
    }];
    AVPlayerViewController *playerViewController;
    playerViewController = [[AVPlayerViewController alloc] init];
    NSLog(@"url%@:",allResource);
//
//    for (NSURL *url in allResource) {
//        [self saveVideoWithUrl:url];
//    }
    playerViewController.player = [AVPlayer playerWithURL:allResource.firstObject];
    //    playerViewController.delegate = self;
    [self presentViewController:playerViewController animated:YES completion:^{
        [playerViewController.player play];
        NSLog(@"error == %@", playerViewController.player.error);
    }];
    
}
- (NSString *)getDocumentPath {
    
    static NSString *replaysPath;
    if (!replaysPath) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *documentRootPath = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.ask.answer.live"];
        replaysPath = [documentRootPath.path stringByAppendingPathComponent:@"Replays"];
        if (![fileManager fileExistsAtPath:replaysPath]) {
            NSError *error_createPath = nil;
            BOOL success_createPath = [fileManager createDirectoryAtPath:replaysPath withIntermediateDirectories:true attributes:@{} error:&error_createPath];
            if (success_createPath && !error_createPath) {
                NSLog(@"%@路径创建成功!", replaysPath);
            } else {
                NSLog(@"%@路径创建失败:%@", replaysPath, error_createPath);
            }
        }
    }
    return replaysPath;
}
- (NSArray <NSURL *> *)fetechAllResource {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    NSString *documentPath = [self getDocumentPath];
    NSURL *documentURL = [NSURL fileURLWithPath:documentPath];
    NSError *error = nil;
    NSArray<NSURL *> *allResource  =  [fileManager contentsOfDirectoryAtURL:documentURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants) error:&error];
    return allResource;
    
}
- (void)saveVideoWithUrl:(NSURL *)url {
    PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
    [photoLibrary performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];
        
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        if (success) {
            NSLog(@"已将视频保存至相册");
        } else {
            NSLog(@"未能保存视频到相册");
        }
    }];
}
posted @ 2022-02-16 12:36  struggle_time  阅读(2661)  评论(0编辑  收藏  举报