1039 lines
39 KiB
Objective-C
1039 lines
39 KiB
Objective-C
//
|
||
// 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 "AVApiService.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) {
|
||
// 校验加密密钥
|
||
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<AVLiveStreamModel *> *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<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
|