// // SCRtmpLiveViewController.m // SellyCloudSDK_Example // // Created by Caleb on 8/7/25. // Copyright © 2025 Caleb. All rights reserved. // #import "SCLivePusherViewController.h" #import #import "SCLiveItemContainerView.h" #import "FUManager.h" #import #import "SCLiveStatsView.h" #import "UIView+SellyCloud.h" #import "AVSettingsView.h" #import "AVConfigManager.h" #import "TokenGenerator.h" #import "AVLiveStreamModel.h" #import "AVConstants.h" #import "AVApiService.h" @interface SCLivePusherViewController () @property (nonatomic, strong)UIView *liveView; @property (nonatomic, strong)SellyLiveVideoPusher *livePusher; @property (nonatomic, strong)SCLiveItemContainerView *itemContainer; @property (nonatomic, strong)SCLiveStatsView *statsView; // Custom UI @property (nonatomic, strong)UIButton *closeButton; @property (nonatomic, strong)UIButton *settingsButton; @property (nonatomic, strong)UIButton *startLiveButton; @property (nonatomic, strong)UIButton *rotateButton; @property (nonatomic, strong)UIButton *switchCameraButton; @property (nonatomic, strong)UIButton *linkButton; // 连麦按钮 @property (nonatomic, assign)BOOL isLiveStarted; @property (nonatomic, assign)BOOL isLandscape; // 记录当前是否为横屏 // 连麦相关 @property (nonatomic, assign, readwrite)BOOL isLinking; @property (nonatomic, strong)SellyLiveVideoPlayer *linkPlayer; // 连麦播放器 @property (nonatomic, strong)UIView *linkPlayerView; // 连麦播放器视图 @property (nonatomic, strong)UIView *localPusherView; // 本地推流视图(用于连麦时的半屏显示) @end @implementation SCLivePusherViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = UIColor.whiteColor; // 隐藏导航栏 self.navigationController.navigationBarHidden = YES; [self.view addSubview:self.liveView]; [self.liveView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 添加自定义按钮 [self setupCustomButtons]; // 禁止息屏 [UIApplication sharedApplication].idleTimerDisabled = YES; [self.view addSubview:self.statsView]; [self.statsView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.offset(10); make.top.offset(50); make.width.offset(220); // 不再固定高度,让内容自动撑开 }]; // 只启动预览,不立即开始直播 [self startPreview]; } - (void)setupCustomButtons { // 关闭按钮(右上角) _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_closeButton setImage:[UIImage systemImageNamed:@"xmark.circle.fill"] forState:UIControlStateNormal]; _closeButton.tintColor = [UIColor whiteColor]; _closeButton.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; // 添加半透明黑色背景 _closeButton.layer.cornerRadius = 22; _closeButton.layer.masksToBounds = YES; [_closeButton addTarget:self action:@selector(closeButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_closeButton]; [_closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.offset(-16); make.top.offset(50); make.width.height.offset(44); }]; // 连麦按钮(右上角,关闭按钮左边) _linkButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_linkButton setImage:[UIImage systemImageNamed:@"person.2.wave.2"] forState:UIControlStateNormal]; [_linkButton setImage:[UIImage systemImageNamed:@"person.2.slash"] forState:UIControlStateSelected]; _linkButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8]; _linkButton.tintColor = [UIColor whiteColor]; _linkButton.layer.cornerRadius = 22; _linkButton.layer.masksToBounds = YES; _linkButton.hidden = YES; // 开始直播后才显示 _linkButton.imageView.backgroundColor = UIColor.clearColor; [_linkButton addTarget:self action:@selector(linkButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_linkButton]; [_linkButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(_closeButton.mas_left).offset(-8); make.centerY.equalTo(_closeButton); make.width.height.offset(44); }]; // 设置按钮(关闭按钮左边) // _settingsButton = [UIButton buttonWithType:UIButtonTypeSystem]; // [_settingsButton setImage:[UIImage systemImageNamed:@"gearshape.fill"] forState:UIControlStateNormal]; // _settingsButton.tintColor = [UIColor whiteColor]; // [_settingsButton addTarget:self action:@selector(settingsButtonTapped) forControlEvents:UIControlEventTouchUpInside]; // [self.view addSubview:_settingsButton]; // [_settingsButton mas_makeConstraints:^(MASConstraintMaker *make) { // make.right.equalTo(_closeButton.mas_left).offset(-8); // make.top.offset(50); // make.width.height.offset(44); // }]; // 开始直播按钮(底部中央) _startLiveButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_startLiveButton setTitle:@"开始直播" forState:UIControlStateNormal]; [_startLiveButton setTitle:@"结束直播" forState:UIControlStateSelected]; _startLiveButton.backgroundColor = [UIColor systemRedColor]; [_startLiveButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_startLiveButton setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected]; _startLiveButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightBold]; _startLiveButton.layer.cornerRadius = 25; _startLiveButton.layer.masksToBounds = YES; [_startLiveButton addTarget:self action:@selector(settingsButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_startLiveButton]; [_startLiveButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view).offset(-60); // 稍微左移为旋转按钮腾出空间 make.bottom.offset(-100); make.width.offset(160); make.height.offset(50); }]; // 旋转按钮(开始直播按钮右边) _rotateButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_rotateButton setImage:[UIImage systemImageNamed:@"rotate.right"] forState:UIControlStateNormal]; _rotateButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8]; _rotateButton.tintColor = [UIColor whiteColor]; _rotateButton.layer.cornerRadius = 25; _rotateButton.layer.masksToBounds = YES; [_rotateButton addTarget:self action:@selector(rotateButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_rotateButton]; [_rotateButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(_startLiveButton.mas_right).offset(16); make.centerY.equalTo(_startLiveButton); make.width.height.offset(50); }]; // 切换前后摄像头按钮(旋转按钮右边) _switchCameraButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_switchCameraButton setImage:[UIImage systemImageNamed:@"camera.rotate"] forState:UIControlStateNormal]; _switchCameraButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8]; _switchCameraButton.tintColor = [UIColor whiteColor]; _switchCameraButton.layer.cornerRadius = 25; _switchCameraButton.layer.masksToBounds = YES; [_switchCameraButton addTarget:self action:@selector(switchCameraButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_switchCameraButton]; [_switchCameraButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(_rotateButton.mas_right).offset(16); make.centerY.equalTo(_startLiveButton); make.width.height.offset(50); }]; // 初始化为竖屏(Portrait Up) self.isLandscape = NO; } - (void)closeButtonTapped { // 如果正在直播,先停止直播 if (self.isLiveStarted) { [self.livePusher stopLive:^(NSError * _Nonnull error) { NSLog(@"直播已停止"); }]; } // 恢复导航栏 self.navigationController.navigationBarHidden = NO; // 返回上一页 [self.navigationController popViewControllerAnimated:YES]; } - (void)startLiveButtonTapped { //保存直播配置 [AVConfigManager.sharedManager saveConfig]; if (!self.isLiveStarted) { // 校验加密密钥 NSString *xorKeyError = [self validateXorKey:self.videoConfig.xorKey]; if (xorKeyError) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"密钥格式错误" message:xorKeyError preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; return; } // 开始直播 [self startLive]; self.isLiveStarted = YES; self.startLiveButton.selected = YES; self.startLiveButton.backgroundColor = [UIColor systemGrayColor]; // 隐藏开始直播按钮和旋转按钮,显示功能按钮 [UIView animateWithDuration:0.3 animations:^{ self.startLiveButton.alpha = 0; self.rotateButton.alpha = 0; self.switchCameraButton.alpha = 0; } completion:^(BOOL finished) { self.startLiveButton.hidden = YES; self.rotateButton.hidden = YES; self.switchCameraButton.hidden = YES; // 显示连麦按钮 self.linkButton.hidden = NO; self.linkButton.alpha = 0; [UIView animateWithDuration:0.3 animations:^{ self.linkButton.alpha = 1; }]; }]; // 显示功能按钮容器 [self setupItemContainer]; } else { // 结束直播前,如果正在连麦,先断开连麦 if (self.isLinking) { [self disconnectLink]; } // 结束直播 [self.livePusher stopLive:^(NSError * _Nonnull error) { NSLog(@"直播已停止"); }]; self.isLiveStarted = NO; self.startLiveButton.selected = NO; self.startLiveButton.backgroundColor = [UIColor systemRedColor]; // 隐藏连麦按钮 [UIView animateWithDuration:0.3 animations:^{ self.linkButton.alpha = 0; } completion:^(BOOL finished) { self.linkButton.hidden = YES; }]; // 显示开始直播按钮和旋转按钮,隐藏功能按钮 self.startLiveButton.hidden = NO; self.rotateButton.hidden = NO; self.switchCameraButton.hidden = NO; [UIView animateWithDuration:0.3 animations:^{ self.startLiveButton.alpha = 1; self.rotateButton.alpha = 1; self.switchCameraButton.alpha = 1; }]; [self.itemContainer removeFromSuperview]; } } - (void)settingsButtonTapped { AVSettingsView *settingsView = [[AVSettingsView alloc] init]; [settingsView showInViewController:self withConfig:self.videoConfig fieldsMask:AVSettingsFieldAll // Show all settings callback:^(AVVideoConfiguration *updatedConfig) { self.videoConfig = updatedConfig; self.livePusher.videoConfig = updatedConfig; NSLog(@"Settings updated: streamId=%@, codec=%ld, resolution=%ld, fps=%ld, maxbitrate == %ld videoSize == %@", self.videoConfig.streamId, (long)self.videoConfig.codec, (long)[self.videoConfig currentResolution], (long)self.videoConfig.videoFrameRate,self.videoConfig.videoBitRate,NSStringFromCGSize(updatedConfig.videoSize)); // 如果已经在直播,重新启动 [self startLiveButtonTapped]; }]; } - (void)rotateButtonTapped { // 在竖屏(上)和横屏(右)之间切换 if (self.isLandscape) { // 当前是横屏,切换到竖屏(Portrait) [self rotateToOrientation:UIInterfaceOrientationPortrait]; } else { // 当前是竖屏,切换到横屏(Landscape Right) [self rotateToOrientation:UIInterfaceOrientationLandscapeRight]; } // 切换状态 self.isLandscape = !self.isLandscape; // 添加旋转动画 [UIView animateWithDuration:0.3 animations:^{ self.rotateButton.transform = CGAffineTransformRotate(self.rotateButton.transform, M_PI); }]; } - (void)switchCameraButtonTapped { // 切换前后摄像头 [self switchDevicePosition]; // 添加翻转动画 [UIView animateWithDuration:0.3 animations:^{ // 水平翻转动画 self.switchCameraButton.transform = CGAffineTransformMakeScale(-1, 1); } completion:^(BOOL finished) { [UIView animateWithDuration:0.3 animations:^{ self.switchCameraButton.transform = CGAffineTransformIdentity; }]; }]; } #pragma mark - Orientation Support (禁用自动旋转,只支持手动) - (BOOL)shouldAutorotate { // 禁用自动旋转 return NO; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { // 支持竖屏和横屏右,但不会自动旋转 return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeRight; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return UIInterfaceOrientationPortrait; } - (void)rotateToOrientation:(UIInterfaceOrientation)orientation { NSLog(@"🔄 切换视频方向: %@", orientation == UIInterfaceOrientationPortrait ? @"竖屏" : @"横屏"); // 更新视频配置 self.videoConfig.outputImageOrientation = orientation; // 强制旋转整个页面 if (@available(iOS 16.0, *)) { // iOS 16+ 使用新 API [self setNeedsUpdateOfSupportedInterfaceOrientations]; NSArray *array = [[[UIApplication sharedApplication] connectedScenes] allObjects]; UIWindowScene *scene = (UIWindowScene *)array.firstObject; UIInterfaceOrientationMask mask; if (orientation == UIInterfaceOrientationPortrait) { mask = UIInterfaceOrientationMaskPortrait; } else if (orientation == UIInterfaceOrientationLandscapeRight) { mask = UIInterfaceOrientationMaskLandscapeRight; } else { mask = UIInterfaceOrientationMaskPortrait; } UIWindowSceneGeometryPreferencesIOS *preferences = [[UIWindowSceneGeometryPreferencesIOS alloc] initWithInterfaceOrientations:mask]; [scene requestGeometryUpdateWithPreferences:preferences errorHandler:^(NSError * _Nonnull error) { NSLog(@"旋转失败: %@", error); }]; } else { // iOS 16 以下使用旧方法 if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) { SEL selector = NSSelectorFromString(@"setOrientation:"); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]]; [invocation setSelector:selector]; [invocation setTarget:[UIDevice currentDevice]]; UIDeviceOrientation deviceOrientation; if (orientation == UIInterfaceOrientationPortrait) { deviceOrientation = UIDeviceOrientationPortrait; } else if (orientation == UIInterfaceOrientationLandscapeRight) { deviceOrientation = UIDeviceOrientationLandscapeLeft; // 注意:UIDeviceOrientation 和 UIInterfaceOrientation 方向相反 } else { deviceOrientation = UIDeviceOrientationPortrait; } [invocation setArgument:&deviceOrientation atIndex:2]; [invocation invoke]; } } } - (void)updatePreviewTransform:(UIInterfaceOrientation)orientation { // 根据方向调整预览视图的 transform CGAffineTransform transform; switch (orientation) { case UIInterfaceOrientationPortrait: transform = CGAffineTransformIdentity; break; case UIInterfaceOrientationPortraitUpsideDown: transform = CGAffineTransformMakeRotation(M_PI); break; case UIInterfaceOrientationLandscapeLeft: transform = CGAffineTransformMakeRotation(-M_PI_2); break; case UIInterfaceOrientationLandscapeRight: transform = CGAffineTransformMakeRotation(M_PI_2); break; default: transform = CGAffineTransformIdentity; break; } // 添加动画效果 [UIView animateWithDuration:0.3 animations:^{ self.liveView.transform = transform; // 横屏时调整 bounds if (UIInterfaceOrientationIsLandscape(orientation)) { CGRect bounds = self.liveView.bounds; self.liveView.bounds = CGRectMake(0, 0, bounds.size.height, bounds.size.width); } else { // 恢复原始 bounds self.liveView.bounds = self.view.bounds; } }]; } - (void)appDidEnterBackground { [self _sendPicture]; } - (void)appWillEnterForeground { [self _stopSendPicture]; } - (void)startPreview { if (self.protocol == AVStreamProtocolRTC) { self.livePusher = [[SellyLiveVideoPusher alloc] initWithLiveMode:SellyLiveMode_RTC]; } else { self.livePusher = [[SellyLiveVideoPusher alloc] initWithLiveMode:SellyLiveMode_RTMP]; } self.livePusher.centerStageEnabled = true; self.livePusher.preview = self.liveView; self.livePusher.delegate = self; self.livePusher.enableCustomVideoProcess = true; self.livePusher.scaleMode = SellyPlayerScalingModeAspectFill; SellyLiveVideoConfiguration *videoConfig = self.videoConfig; if (self.audioOnly) { //纯语音直播 [self.livePusher startRunningAudio:nil]; } else { //音视频直播 [self.livePusher startRunning:AVCaptureDevicePositionBack videoConfig:videoConfig audioConfig:nil]; } } - (void)startLive { self.statsView.streamId = self.videoConfig.streamId; NSString *token = [TokenGenerator generateStreamSignatureWithVhost:V_HOST appId:APP_ID channelId:self.videoConfig.streamId type:@"push" key:APP_SECRET]; self.livePusher.token = token; self.livePusher.xorKey = self.videoConfig.xorKey; NSError *error; if (self.videoConfig.streamId) { error = [self.livePusher startLiveWithStreamId:self.videoConfig.streamId]; } else { // error = [self.livePusher startLiveWithUrl:<#(nonnull NSString *)#>]; } if (error) { NSLog(@"###startLive failed. error == %@",error.localizedDescription); } // 上报 XOR key 到服务器 [self reportXorKey]; } /// 校验 xorKey 格式,返回 nil 表示合法,返回错误信息表示不合法 - (NSString *)validateXorKey:(NSString *)xorKey { if (xorKey.length == 0) return nil; if (xorKey.length % 2 != 0) { return @"加密密钥必须为偶数长度"; } NSCharacterSet *hexChars = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"]; if ([xorKey rangeOfCharacterFromSet:hexChars.invertedSet].location != NSNotFound) { return @"加密密钥只能包含十六进制字符 (0-9, a-f, A-F)"; } return nil; } - (void)reportXorKey { [[AVApiService shared] reportXorKeyWithVhost:V_HOST app:APP_ID stream:self.videoConfig.streamId xorKey:self.videoConfig.xorKey success:nil failure:nil]; } - (void)setupItemContainer { __weak typeof(self) weakSelf = self; NSMutableArray *items = NSMutableArray.new; { SCLiveItemModel *model = SCLiveItemModel.new; model.type = SCLiveItemTypeSwitchCamera; model.title = @"翻转"; model.clickCallback = ^{ [weakSelf switchDevicePosition]; }; [items addObject:model]; } { SCLiveItemModel *model = SCLiveItemModel.new; model.type = SCLiveItemTypeMute; model.title = @"静音"; model.isSelected = self.livePusher.isMute; model.clickCallback = ^{ [weakSelf muteClick]; }; [items addObject:model]; } { SCLiveItemModel *model = SCLiveItemModel.new; model.type = SCLiveItemTypeCameraToggle; model.title = @"摄像头"; model.isSelected = !self.livePusher.isCameraEnable; model.clickCallback = ^{ [weakSelf cameraClick]; }; [items addObject:model]; } { SCLiveItemModel *model = SCLiveItemModel.new; model.type = SCLiveItemTypeCapture; model.title = @"截图"; model.clickCallback = ^{ [weakSelf captureImageClick]; }; [items addObject:model]; } { SCLiveItemModel *model = SCLiveItemModel.new; model.type = SCLiveItemTypeBackgroundImage; model.title = @"背景图"; model.clickCallback = ^{ [weakSelf sendStaticImage]; }; [items addObject:model]; } self.itemContainer.models = items; [self.view addSubview:self.itemContainer]; [self.itemContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.left.offset(16); make.right.offset(-16); make.bottom.offset(-40); make.height.offset(88); }]; } - (void)switchDevicePosition { [self.livePusher switchCameraPosition:nil]; if (self.livePusher.captureDevicePosition == AVCaptureDevicePositionFront) { self.livePusher.mirror = true; } else { self.livePusher.mirror = false; } } - (void)muteClick { if (self.livePusher.isMute) { [self.livePusher startMicrophone]; } else { [self.livePusher stopMicrophone]; } } - (void)cameraClick { if (self.livePusher.isCameraEnable) { [self.livePusher stopCamera]; } else { [self.livePusher startCamera]; } } - (void)mirrorClick { self.livePusher.mirror = !self.livePusher.mirror; } - (void)captureImageClick { [self saveCurrentFrameToPhotoAlbum:[self.livePusher getCurrentImage]]; } - (void)sendStaticImage { if (self.livePusher.isCameraEnable) { //推送静态图片 [self.livePusher stopCamera]; [self _sendPicture]; } else { //推送相机采集流 [self _stopSendPicture]; [self.livePusher startCamera]; } } - (void)_sendPicture { [self.livePusher pushStaticImage:[UIImage imageNamed:@"test.jpg"]]; } - (void)_stopSendPicture { [self.livePusher stopPushImage]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 如果当前是横屏,强制旋转回竖屏 if (self.isLandscape) { [self rotateToOrientation:UIInterfaceOrientationPortrait]; self.isLandscape = NO; } } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // 恢复导航栏 self.navigationController.navigationBarHidden = NO; // 如果正在连麦,先断开连麦 if (self.isLinking) { [self disconnectLink]; } if (self.isLiveStarted) { [self.livePusher stopLive:^(NSError * _Nonnull error) { NSLog(@"直播已停止"); }]; } } // 假设 self.session 是你的 LFLiveSession 实例 - (void)saveCurrentFrameToPhotoAlbum:(UIImage *)image { if (!image) { NSLog(@"当前没有图像可保存"); return; } // 检查权限 PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; if (status == PHAuthorizationStatusDenied || status == PHAuthorizationStatusRestricted) { NSLog(@"无权限访问相册,请到设置中开启权限"); return; } if (status == PHAuthorizationStatusNotDetermined) { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus newStatus) { if (newStatus == PHAuthorizationStatusAuthorized) { [self saveImage:image]; } }]; } else { [self saveImage:image]; } } - (void)saveImage:(UIImage *)image { UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL); } // 保存完成回调 - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { if (error) { NSLog(@"保存失败:%@", error.localizedDescription); } else { NSLog(@"已保存当前视频帧至相册"); } } - (void)dealloc { // 禁止息屏 [UIApplication sharedApplication].idleTimerDisabled = NO; [NSNotificationCenter.defaultCenter removeObserver:self]; NSLog(@"###%@ dealloc",NSStringFromClass(self.class)); } /* #pragma mark - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { // Get the new view controller using [segue destinationViewController]. // Pass the selected object to the new view controller. } */ #pragma marks SellyCloudRTMPDelegate - (void)pusher:(SellyLiveVideoPusher *)pusher liveStatusDidChanged:(SellyLiveState)status { } - (void)pusher:(SellyLiveVideoPusher *)pusher onStatisticsUpdate:(SellyLivePusherStats *)stats { //NSLog(@"##stats == %@",stats); self.statsView.stats = stats; } - (void)pusher:(SellyLiveVideoPusher *)pusher onError:(NSError *)error { //推流报错,直播结束 NSLog(@"###onPusherError == %@",error); [self.navigationController popViewControllerAnimated:true]; [UIApplication.sharedApplication.delegate.window showToast:error.localizedDescription]; } - (CVPixelBufferRef)pusher:(SellyLiveVideoPusher *)pusher onCaptureVideoFrame:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRef afterBuffer = [FUManager.shareManager renderItemsToPixelBuffer:pixelBuffer]; return afterBuffer; } - (SCLiveItemContainerView *)itemContainer { if (!_itemContainer) { _itemContainer = [[SCLiveItemContainerView alloc] init]; } return _itemContainer; } - (UIView *)liveView { if (!_liveView) { _liveView = UIView.new; } return _liveView; } - (SCLiveStatsView *)statsView { if (!_statsView) { _statsView = [[SCLiveStatsView alloc] init]; } return _statsView; } #pragma mark - 连麦相关方法 - (void)linkButtonTapped { if (!self.isLinking) { // 开始连麦:显示直播列表选择器 [self showLiveStreamSelector]; } else { // 断开连麦 [self disconnectLink]; } } - (void)showLiveStreamSelector { // 显示加载提示 UIAlertController *loadingAlert = [UIAlertController alertControllerWithTitle:@"加载中" message:@"正在获取直播列表..." preferredStyle:UIAlertControllerStyleAlert]; [self presentViewController:loadingAlert animated:YES completion:nil]; // 获取直播列表 __weak typeof(self) weakSelf = self; [[AVApiService shared] fetchLiveStreams:^(NSArray *streams) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; [loadingAlert dismissViewControllerAnimated:YES completion:^{ if (streams.count == 0) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:@"暂无正在直播的用户" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [strongSelf presentViewController:alert animated:YES completion:nil]; } else { [strongSelf showStreamListWithStreams:streams]; } }]; } failure:^(NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; [loadingAlert dismissViewControllerAnimated:YES completion:^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败" message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [strongSelf presentViewController:alert animated:YES completion:nil]; }]; }]; } - (void)showStreamListWithStreams:(NSArray *)streams { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"选择连麦用户" message:@"请选择要连麦的直播流" preferredStyle:UIAlertControllerStyleActionSheet]; __weak typeof(self) weakSelf = self; // 添加每个直播流为选项 for (AVLiveStreamModel *stream in streams) { NSString *title = [NSString stringWithFormat:@"流ID: %@", stream.stream]; UIAlertAction *action = [UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; NSLog(@"🎤 选中连麦流: %@", stream.stream); // 开始连麦 [strongSelf startLinkWithStream:stream]; }]; [alert addAction:action]; } // 取消按钮 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]; [alert addAction:cancelAction]; // 在 iPad 上需要设置 popoverPresentationController if (alert.popoverPresentationController) { alert.popoverPresentationController.sourceView = self.view; alert.popoverPresentationController.sourceRect = CGRectMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0, 1.0, 1.0); alert.popoverPresentationController.permittedArrowDirections = 0; } [self presentViewController:alert animated:YES completion:nil]; } - (void)startLinkWithStream:(AVLiveStreamModel *)stream { self.linkingStream = stream; self.isLinking = YES; // 更新连麦按钮状态 self.linkButton.selected = YES; self.linkButton.backgroundColor = [UIColor systemRedColor]; // 创建连麦播放器视图和本地推流视图 [self setupLinkViews]; // 开始播放连麦用户的流 [self startLinkPlayer]; } - (void)setupLinkViews { CGFloat screenWidth = self.view.bounds.size.width; CGFloat screenHeight = self.view.bounds.size.height; // 计算单个视图的尺寸(宽高比 9:16) CGFloat space = 8; // 中间间距 CGFloat viewWidth = (screenWidth - space) / 2.0; // 屏幕宽度去掉间距后的一半 CGFloat viewHeight = viewWidth * (16.0 / 9.0); // 宽高比 9:16 // 确保高度不超过屏幕高度 if (viewHeight > screenHeight) { viewHeight = screenHeight; viewWidth = viewHeight * (9.0 / 16.0); } // 顶部偏移量(从顶部开始,留出安全区域和一些边距) CGFloat topOffset = self.view.safeAreaInsets.top + 80; // 80pt 为顶部按钮区域高度 // 创建本地推流视图(左侧) if (!self.localPusherView) { self.localPusherView = [[UIView alloc] init]; self.localPusherView.backgroundColor = [UIColor blackColor]; self.localPusherView.layer.cornerRadius = 8; self.localPusherView.clipsToBounds = YES; [self.view insertSubview:self.localPusherView belowSubview:self.closeButton]; [self.localPusherView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(0); make.top.equalTo(self.view).offset(topOffset); // 从顶部开始 make.width.equalTo(@(viewWidth)); make.height.equalTo(@(viewHeight)); }]; } // 创建连麦播放器视图(右侧) if (!self.linkPlayerView) { self.linkPlayerView = [[UIView alloc] init]; self.linkPlayerView.backgroundColor = [UIColor blackColor]; self.linkPlayerView.layer.cornerRadius = 8; self.linkPlayerView.clipsToBounds = YES; [self.view insertSubview:self.linkPlayerView belowSubview:self.closeButton]; [self.linkPlayerView mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.view).offset(0); make.top.equalTo(self.view).offset(topOffset); // 从顶部开始 make.width.equalTo(@(viewWidth)); make.height.equalTo(@(viewHeight)); }]; } // 将原来的 liveView 移动到 localPusherView [self.liveView removeFromSuperview]; [self.localPusherView addSubview:self.liveView]; [self.liveView mas_remakeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.localPusherView); }]; // 添加动画效果 self.localPusherView.alpha = 0; self.linkPlayerView.alpha = 0; [UIView animateWithDuration:0.3 animations:^{ self.localPusherView.alpha = 1; self.linkPlayerView.alpha = 1; }]; NSLog(@"✅ 连麦视图已创建(居上显示) - 宽度: %.0f, 高度: %.0f, 顶部偏移: %.0f", viewWidth, viewHeight, topOffset); } - (void)startLinkPlayer { if (!self.linkingStream) { NSLog(@"❌ 没有连麦流信息"); return; } // 创建播放器 self.linkPlayer = [[SellyLiveVideoPlayer alloc] init]; self.linkPlayer.delegate = self; self.linkPlayer.scaleMode = SellyPlayerScalingModeAspectFill; // 设置渲染视图 [self.linkPlayer setRenderView:self.linkPlayerView]; // 生成播放 token NSString *token = [TokenGenerator generateStreamSignatureWithVhost:self.linkingStream.vhost appId:self.linkingStream.app channelId:self.linkingStream.stream type:@"pull" key:APP_SECRET]; self.linkPlayer.token = token; if (self.linkingStream.url) { [self.linkPlayer startPlayUrl:self.linkingStream.url]; } else { // 创建流信息 SellyPlayerStreamInfo *streamInfo = [[SellyPlayerStreamInfo alloc] init]; streamInfo.streamId = self.linkingStream.stream; // 设置协议 if ([self.linkingStream.play_protocol.lowercaseString isEqualToString:@"rtc"]) { streamInfo.protocol = SellyLiveMode_RTC; } else { streamInfo.protocol = SellyLiveMode_RTMP; } // 开始播放 [self.linkPlayer startPlayStreamInfo:streamInfo]; } // NSLog(@"🎬 开始播放连麦用户流 - vhost: %@, app: %@, stream: %@, protocol: %@", // streamInfo.vhost, streamInfo.app, streamInfo.streamId, self.linkingStream.play_protocol); } - (void)disconnectLink { NSLog(@"🔌 断开连麦"); self.isLinking = NO; self.linkingStream = nil; // 更新连麦按钮状态 self.linkButton.selected = NO; self.linkButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8]; // 停止播放器 if (self.linkPlayer) { [self.linkPlayer stop]; self.linkPlayer = nil; } // 移除视图 [UIView animateWithDuration:0.3 animations:^{ self.localPusherView.alpha = 0; self.linkPlayerView.alpha = 0; } completion:^(BOOL finished) { // 将 liveView 恢复到原来的位置 [self.liveView removeFromSuperview]; [self.view insertSubview:self.liveView atIndex:0]; [self.liveView mas_remakeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 移除连麦视图 [self.localPusherView removeFromSuperview]; self.localPusherView = nil; [self.linkPlayerView removeFromSuperview]; self.linkPlayerView = nil; // 恢复 liveView 的透明度 [UIView animateWithDuration:0.3 animations:^{ self.liveView.alpha = 1; }]; }]; } #pragma mark - SellyLivePlayerDelegate (连麦播放器代理) - (void)player:(SellyLiveVideoPlayer *)player playbackStateDidChanged:(SellyPlayerState)state { NSLog(@"🎬 连麦播放器状态变化: %ld", (long)state); if (state == SellyPlayerStateConnecting) { NSLog(@"⏳ 连麦播放器连接中..."); } else if (state == SellyPlayerStatePlaying) { NSLog(@"▶️ 连麦播放器播放中"); } else if (state == SellyPlayerStatePaused) { NSLog(@"⏸ 连麦播放器已暂停"); } else if (state == SellyPlayerStateStoppedOrEnded) { NSLog(@"⏹ 连麦播放器已停止"); } else if (state == SellyPlayerStateFailed) { NSLog(@"❌ 连麦播放器错误"); // 自动断开连麦 [self disconnectLink]; // 提示用户 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"连麦失败" message:@"无法连接到对方的直播流" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } } //- (void)player:(SellyLiveVideoPlayer *)player onStatisticsUpdate:(SellyPlayerStats *)stats { // // 连麦播放器的统计信息(可选) //} @end