Files
SellyCloudSDK_demo/Example/SellyCloudSDK/Live/SCLivePusherViewController.m
2026-03-01 15:59:27 +08:00

1023 lines
38 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// SCRtmpLiveViewController.m
// SellyCloudSDK_Example
//
// Created by Caleb on 8/7/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SCLivePusherViewController.h"
#import <SellyCloudSDK/SellyCloudManager.h>
#import "SCLiveItemContainerView.h"
#import "FUManager.h"
#import <Photos/Photos.h>
#import "SCLiveStatsView.h"
#import "UIView+SellyCloud.h"
#import "AVSettingsView.h"
#import "AVConfigManager.h"
#import "TokenGenerator.h"
#import "AVLiveStreamModel.h"
#import "AVConstants.h"
#import <AFNetworking/AFNetworking.h>
#import <YYModel/YYModel.h>
@interface SCLivePusherViewController ()<SellyLivePusherDelegate, SellyLivePlayerDelegate>
@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) {
// 开始直播
[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.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;
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);
}
}
- (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];
// 获取直播列表
AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
sessionManager.requestSerializer.timeoutInterval = 10;
__weak typeof(self) weakSelf = self;
NSString *apiURL = @"http://rtmp.sellycloud.io:8089/live/sdk/alive-list";
[sessionManager GET:apiURL parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *responseObject) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
// 关闭加载提示
[loadingAlert dismissViewControllerAnimated:YES completion:^{
// 解析数据
NSArray<AVLiveStreamModel *> *liveStreams = [NSArray yy_modelArrayWithClass:AVLiveStreamModel.class json:responseObject[@"list"]];
NSLog(@"✅ 获取 %ld 个直播流", (long)liveStreams.count);
if (liveStreams.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:liveStreams];
}
}];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
// 关闭加载提示
[loadingAlert dismissViewControllerAnimated:YES completion:^{
NSLog(@"❌ 获取直播列表失败: %@", error.localizedDescription);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
message:[NSString stringWithFormat:@"无法获取直播列表: %@", error.localizedDescription]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
[strongSelf presentViewController:alert animated:YES completion:nil];
}];
}];
}
- (void)showStreamListWithStreams:(NSArray<AVLiveStreamModel *> *)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