rtmp、rtc推拉流支持加密
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0897849CBA2960C1F1BE2DC4 /* Pods_SellyCloudSDK_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3A5DE9B7559BAE46EA68112 /* Pods_SellyCloudSDK_Tests.framework */; };
|
0897849CBA2960C1F1BE2DC4 /* Pods_SellyCloudSDK_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3A5DE9B7559BAE46EA68112 /* Pods_SellyCloudSDK_Tests.framework */; };
|
||||||
|
0D0ECA322F83CFDC00917568 /* AVApiService.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D0ECA302F83CFDC00917568 /* AVApiService.m */; };
|
||||||
236246B1275D4CD3B91DACE1 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 353107A80A9449DFB01DACE1 /* README.md */; };
|
236246B1275D4CD3B91DACE1 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 353107A80A9449DFB01DACE1 /* README.md */; };
|
||||||
3C075C2B2E3873A800591B2D /* test1.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C075C2A2E3873A800591B2D /* test1.png */; };
|
3C075C2B2E3873A800591B2D /* test1.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C075C2A2E3873A800591B2D /* test1.png */; };
|
||||||
3C0F91622EF39F0000680CB7 /* SCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C0F91612EF39F0000680CB7 /* SCNavigationController.m */; };
|
3C0F91622EF39F0000680CB7 /* SCNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C0F91612EF39F0000680CB7 /* SCNavigationController.m */; };
|
||||||
@@ -122,6 +123,8 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
0D0ECA2F2F83CFDC00917568 /* AVApiService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AVApiService.h; sourceTree = "<group>"; };
|
||||||
|
0D0ECA302F83CFDC00917568 /* AVApiService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AVApiService.m; sourceTree = "<group>"; };
|
||||||
11F5CE3EA9A94E55D1A35A8F /* SellyCloudSDK.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SellyCloudSDK.podspec; path = ../SellyCloudSDK.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
11F5CE3EA9A94E55D1A35A8F /* SellyCloudSDK.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SellyCloudSDK.podspec; path = ../SellyCloudSDK.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||||
18585A8A04A38911555AC335 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
18585A8A04A38911555AC335 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||||
28EA6D0E21FA7AAFE4E2C53C /* Pods-SellyCloudSDK_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SellyCloudSDK_Example.release.xcconfig"; path = "Target Support Files/Pods-SellyCloudSDK_Example/Pods-SellyCloudSDK_Example.release.xcconfig"; sourceTree = "<group>"; };
|
28EA6D0E21FA7AAFE4E2C53C /* Pods-SellyCloudSDK_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SellyCloudSDK_Example.release.xcconfig"; path = "Target Support Files/Pods-SellyCloudSDK_Example/Pods-SellyCloudSDK_Example.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
@@ -314,6 +317,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
0D0ECA312F83CFDC00917568 /* Network */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0D0ECA2F2F83CFDC00917568 /* AVApiService.h */,
|
||||||
|
0D0ECA302F83CFDC00917568 /* AVApiService.m */,
|
||||||
|
);
|
||||||
|
path = Network;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3C075D132E3B474A00591B2D /* Play */ = {
|
3C075D132E3B474A00591B2D /* Play */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -519,6 +531,7 @@
|
|||||||
6003F593195388D20070C39A /* Example for SellyCloudSDK */ = {
|
6003F593195388D20070C39A /* Example for SellyCloudSDK */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0D0ECA312F83CFDC00917568 /* Network */,
|
||||||
3CC732F92EF0EEF4000027B2 /* Controllers */,
|
3CC732F92EF0EEF4000027B2 /* Controllers */,
|
||||||
3CC732FE2EF0EEF4000027B2 /* Managers */,
|
3CC732FE2EF0EEF4000027B2 /* Managers */,
|
||||||
3CC733052EF0EEF4000027B2 /* Utils */,
|
3CC733052EF0EEF4000027B2 /* Utils */,
|
||||||
@@ -984,6 +997,7 @@
|
|||||||
3C1851E02ECDE7690022F536 /* SellyCallPiPManager.m in Sources */,
|
3C1851E02ECDE7690022F536 /* SellyCallPiPManager.m in Sources */,
|
||||||
3CC7335F2EF24A4C000027B2 /* SellyCallStatsView.m in Sources */,
|
3CC7335F2EF24A4C000027B2 /* SellyCallStatsView.m in Sources */,
|
||||||
3C4BF3132EC347510095F93A /* SellyVideoCallConferenceController.m in Sources */,
|
3C4BF3132EC347510095F93A /* SellyVideoCallConferenceController.m in Sources */,
|
||||||
|
0D0ECA322F83CFDC00917568 /* AVApiService.m in Sources */,
|
||||||
3CDB90672EFF908100FBC4E6 /* AVLiveStreamModel.m in Sources */,
|
3CDB90672EFF908100FBC4E6 /* AVLiveStreamModel.m in Sources */,
|
||||||
3C8AC1F12EB85E4E000A58F1 /* SellyVideoCallViewController.m in Sources */,
|
3C8AC1F12EB85E4E000A58F1 /* SellyVideoCallViewController.m in Sources */,
|
||||||
3C312FFC2F021216006C90A4 /* AVLoginViewController.m in Sources */,
|
3C312FFC2F021216006C90A4 /* AVLoginViewController.m in Sources */,
|
||||||
|
|||||||
@@ -4,15 +4,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "AVUserManager.h"
|
#import "AVUserManager.h"
|
||||||
#import <AFNetworking/AFNetworking.h>
|
#import "AVApiService.h"
|
||||||
|
|
||||||
static NSString * const kUserLoggedInKey = @"AVUserLoggedIn";
|
static NSString * const kUserLoggedInKey = @"AVUserLoggedIn";
|
||||||
static NSString * const kUserUsernameKey = @"AVUserUsername";
|
static NSString * const kUserUsernameKey = @"AVUserUsername";
|
||||||
|
|
||||||
@interface AVUserManager ()
|
|
||||||
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation AVUserManager
|
@implementation AVUserManager
|
||||||
|
|
||||||
+ (instancetype)sharedManager {
|
+ (instancetype)sharedManager {
|
||||||
@@ -27,15 +23,8 @@ static NSString * const kUserUsernameKey = @"AVUserUsername";
|
|||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
self = [super init];
|
self = [super init];
|
||||||
if (self) {
|
if (self) {
|
||||||
// 从 UserDefaults 读取登录状态
|
|
||||||
_isLoggedIn = [[NSUserDefaults standardUserDefaults] boolForKey:kUserLoggedInKey];
|
_isLoggedIn = [[NSUserDefaults standardUserDefaults] boolForKey:kUserLoggedInKey];
|
||||||
_currentUsername = [[NSUserDefaults standardUserDefaults] stringForKey:kUserUsernameKey];
|
_currentUsername = [[NSUserDefaults standardUserDefaults] stringForKey:kUserUsernameKey];
|
||||||
|
|
||||||
// 初始化网络请求管理器
|
|
||||||
self.sessionManager = [AFHTTPSessionManager manager];
|
|
||||||
self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
|
||||||
self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
|
|
||||||
self.sessionManager.requestSerializer.timeoutInterval = 10;
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -43,89 +32,24 @@ static NSString * const kUserUsernameKey = @"AVUserUsername";
|
|||||||
- (void)loginWithUsername:(NSString *)username
|
- (void)loginWithUsername:(NSString *)username
|
||||||
password:(NSString *)password
|
password:(NSString *)password
|
||||||
completion:(void (^)(BOOL success, NSString * _Nullable errorMessage))completion {
|
completion:(void (^)(BOOL success, NSString * _Nullable errorMessage))completion {
|
||||||
|
|
||||||
// 基本参数验证
|
|
||||||
if (username.length == 0) {
|
if (username.length == 0) {
|
||||||
if (completion) {
|
if (completion) completion(NO, @"用户名不能为空");
|
||||||
completion(NO, @"用户名不能为空");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length == 0) {
|
if (password.length == 0) {
|
||||||
if (completion) {
|
if (completion) completion(NO, @"密码不能为空");
|
||||||
completion(NO, @"密码不能为空");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录 API 端点
|
[[AVApiService shared] loginWithUsername:username password:password
|
||||||
NSString *loginURL = @"http://rtmp.sellycloud.io:8089/live/sdk/demo/login";
|
success:^(id responseObject) {
|
||||||
|
[self saveLoginStateWithUsername:username];
|
||||||
// 请求参数
|
if (completion) completion(YES, nil);
|
||||||
NSDictionary *parameters = @{
|
|
||||||
@"username": username,
|
|
||||||
@"password": password
|
|
||||||
};
|
|
||||||
|
|
||||||
NSLog(@"🚀 开始登录请求: %@", username);
|
|
||||||
|
|
||||||
// 发起 POST 请求
|
|
||||||
[self.sessionManager POST:loginURL parameters:parameters headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
|
|
||||||
// 登录成功
|
|
||||||
NSLog(@"✅ 登录成功,服务器响应: %@", responseObject);
|
|
||||||
|
|
||||||
// 保存登录状态
|
|
||||||
[self saveLoginStateWithUsername:username];
|
|
||||||
|
|
||||||
// 可以根据需要保存服务器返回的其他信息(如 token、userId 等)
|
|
||||||
// 例如:
|
|
||||||
// if (responseObject[@"token"]) {
|
|
||||||
// [[NSUserDefaults standardUserDefaults] setObject:responseObject[@"token"] forKey:@"AVUserToken"];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 回调成功
|
|
||||||
if (completion) {
|
|
||||||
completion(YES, nil);
|
|
||||||
}
|
}
|
||||||
|
failure:^(NSError *error, NSString *serverMessage) {
|
||||||
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
|
NSString *msg = serverMessage ?: [NSString stringWithFormat:@"网络请求失败: %@", error.localizedDescription];
|
||||||
// 登录失败
|
if (completion) completion(NO, msg);
|
||||||
NSLog(@"❌ 登录失败: %@", error.localizedDescription);
|
}];
|
||||||
|
|
||||||
NSString *errorMessage = @"登录失败,请检查用户名和密码";
|
|
||||||
|
|
||||||
// 尝试获取 HTTP 状态码
|
|
||||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
|
|
||||||
if (httpResponse) {
|
|
||||||
NSLog(@"❌ HTTP 状态码: %ld", (long)httpResponse.statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析服务器返回的错误信息
|
|
||||||
if (error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]) {
|
|
||||||
NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
|
|
||||||
NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData options:0 error:nil];
|
|
||||||
|
|
||||||
NSLog(@"❌ 服务器错误响应: %@", errorDict);
|
|
||||||
|
|
||||||
// 尝试多种可能的错误信息字段
|
|
||||||
if (errorDict[@"message"]) {
|
|
||||||
errorMessage = errorDict[@"message"];
|
|
||||||
} else if (errorDict[@"error"]) {
|
|
||||||
errorMessage = errorDict[@"error"];
|
|
||||||
} else if (errorDict[@"msg"]) {
|
|
||||||
errorMessage = errorDict[@"msg"];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 网络错误
|
|
||||||
errorMessage = [NSString stringWithFormat:@"网络请求失败: %@", error.localizedDescription];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回调失败
|
|
||||||
if (completion) {
|
|
||||||
completion(NO, errorMessage);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)saveLoginStateWithUsername:(NSString *)username {
|
- (void)saveLoginStateWithUsername:(NSString *)username {
|
||||||
|
|||||||
@@ -13,9 +13,7 @@
|
|||||||
#import "AVLiveStreamModel.h"
|
#import "AVLiveStreamModel.h"
|
||||||
#import "AVLiveStreamCell.h"
|
#import "AVLiveStreamCell.h"
|
||||||
#import "SCPlayerConfigView.h"
|
#import "SCPlayerConfigView.h"
|
||||||
#import <AFNetworking/AFNetworking.h>
|
#import "AVApiService.h"
|
||||||
#import <AFNetworking/UIImageView+AFNetworking.h>
|
|
||||||
#import <YYModel/YYModel.h>
|
|
||||||
|
|
||||||
@interface AVHomeViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
|
@interface AVHomeViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
|
||||||
@property (nonatomic, strong) UIView *headerView;
|
@property (nonatomic, strong) UIView *headerView;
|
||||||
@@ -23,13 +21,11 @@
|
|||||||
@property (nonatomic, strong) UICollectionView *collectionView;
|
@property (nonatomic, strong) UICollectionView *collectionView;
|
||||||
@property (nonatomic, strong) NSArray<AVLiveStreamModel *> *liveStreams;
|
@property (nonatomic, strong) NSArray<AVLiveStreamModel *> *liveStreams;
|
||||||
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
@property (nonatomic, strong) UIRefreshControl *refreshControl;
|
||||||
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation AVHomeViewController
|
@implementation AVHomeViewController
|
||||||
|
|
||||||
static NSString * const kLiveStreamCellIdentifier = @"LiveStreamCell";
|
static NSString * const kLiveStreamCellIdentifier = @"LiveStreamCell";
|
||||||
static NSString * const kLiveListAPIURL = @"http://rtmp.sellycloud.io:8089/live/sdk/alive-list";
|
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
@@ -53,11 +49,6 @@ static NSString * const kLiveListAPIURL = @"http://rtmp.sellycloud.io:8089/live/
|
|||||||
// 初始化数据
|
// 初始化数据
|
||||||
self.liveStreams = [NSMutableArray array];
|
self.liveStreams = [NSMutableArray array];
|
||||||
|
|
||||||
// 设置 AFNetworking
|
|
||||||
self.sessionManager = [AFHTTPSessionManager manager];
|
|
||||||
self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
|
||||||
self.sessionManager.requestSerializer.timeoutInterval = 10;
|
|
||||||
|
|
||||||
[self setupHeaderButtons];
|
[self setupHeaderButtons];
|
||||||
[self setupCollectionView];
|
[self setupCollectionView];
|
||||||
|
|
||||||
@@ -191,30 +182,20 @@ static NSString * const kLiveListAPIURL = @"http://rtmp.sellycloud.io:8089/live/
|
|||||||
- (void)fetchLiveStreams {
|
- (void)fetchLiveStreams {
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
|
|
||||||
[self.sessionManager GET:kLiveListAPIURL parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *responseObject) {
|
[[AVApiService shared] fetchLiveStreams:^(NSArray<AVLiveStreamModel *> *streams) {
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||||
if (!strongSelf) return;
|
if (!strongSelf) return;
|
||||||
NSLog(@"%@",responseObject);
|
|
||||||
[strongSelf.refreshControl endRefreshing];
|
[strongSelf.refreshControl endRefreshing];
|
||||||
// 解析数据
|
strongSelf.liveStreams = streams;
|
||||||
strongSelf.liveStreams = [NSArray yy_modelArrayWithClass:AVLiveStreamModel.class json:responseObject[@"list"]];
|
NSLog(@"✅ 成功获取 %ld 个直播流", (long)streams.count);
|
||||||
|
|
||||||
NSLog(@"✅ 成功获取 %ld 个直播流", (long)strongSelf.liveStreams.count);
|
|
||||||
|
|
||||||
// 刷新 UI
|
|
||||||
[strongSelf.collectionView reloadData];
|
[strongSelf.collectionView reloadData];
|
||||||
|
} failure:^(NSError *error) {
|
||||||
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||||
if (!strongSelf) return;
|
if (!strongSelf) return;
|
||||||
|
|
||||||
[strongSelf.refreshControl endRefreshing];
|
[strongSelf.refreshControl endRefreshing];
|
||||||
|
|
||||||
NSLog(@"❌ 网络请求失败: %@", error.localizedDescription);
|
NSLog(@"❌ 网络请求失败: %@", error.localizedDescription);
|
||||||
|
|
||||||
// 显示错误提示
|
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
|
||||||
message:[NSString stringWithFormat:@"无法获取直播列表: %@", error.localizedDescription]
|
message:error.localizedDescription
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||||
[strongSelf presentViewController:alert animated:YES completion:nil];
|
[strongSelf presentViewController:alert animated:YES completion:nil];
|
||||||
@@ -349,6 +330,7 @@ static NSString * const kLiveListAPIURL = @"http://rtmp.sellycloud.io:8089/live/
|
|||||||
liveStream.preview_image = @"";
|
liveStream.preview_image = @"";
|
||||||
liveStream.duration = 0;
|
liveStream.duration = 0;
|
||||||
liveStream.startTime = [[NSDate date] timeIntervalSince1970];
|
liveStream.startTime = [[NSDate date] timeIntervalSince1970];
|
||||||
|
liveStream.xorKey = config.xorKey;
|
||||||
|
|
||||||
// 根据协议类型设置 playProtocol
|
// 根据协议类型设置 playProtocol
|
||||||
switch (config.protocol) {
|
switch (config.protocol) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
@property (nonatomic, strong) UILabel *nameLabel;
|
@property (nonatomic, strong) UILabel *nameLabel;
|
||||||
@property (nonatomic, strong) UILabel *liveLabel;
|
@property (nonatomic, strong) UILabel *liveLabel;
|
||||||
@property (nonatomic, strong) UILabel *protocolLabel;
|
@property (nonatomic, strong) UILabel *protocolLabel;
|
||||||
|
@property (nonatomic, strong) UIImageView *encryptIcon;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -141,6 +142,19 @@
|
|||||||
make.centerY.equalTo(_liveLabel);
|
make.centerY.equalTo(_liveLabel);
|
||||||
make.height.equalTo(@16);
|
make.height.equalTo(@16);
|
||||||
make.width.greaterThanOrEqualTo(@40);
|
make.width.greaterThanOrEqualTo(@40);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 加密图标
|
||||||
|
_encryptIcon = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"lock.shield.fill"]];
|
||||||
|
_encryptIcon.tintColor = [UIColor systemOrangeColor];
|
||||||
|
_encryptIcon.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
|
_encryptIcon.hidden = YES;
|
||||||
|
[infoContainerView addSubview:_encryptIcon];
|
||||||
|
|
||||||
|
[_encryptIcon mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.left.equalTo(_protocolLabel.mas_right).offset(6);
|
||||||
|
make.centerY.equalTo(_liveLabel);
|
||||||
|
make.width.height.equalTo(@20);
|
||||||
make.right.lessThanOrEqualTo(infoContainerView);
|
make.right.lessThanOrEqualTo(infoContainerView);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -154,6 +168,9 @@
|
|||||||
|
|
||||||
// 设置协议标签
|
// 设置协议标签
|
||||||
_protocolLabel.text = model.play_protocol.uppercaseString;
|
_protocolLabel.text = model.play_protocol.uppercaseString;
|
||||||
|
|
||||||
|
// 加密图标
|
||||||
|
_encryptIcon.hidden = (model.xorKey.length == 0);
|
||||||
|
|
||||||
// 使用 SDWebImage 加载预览图
|
// 使用 SDWebImage 加载预览图
|
||||||
if (model.preview_image.length > 0) {
|
if (model.preview_image.length > 0) {
|
||||||
@@ -177,6 +194,7 @@
|
|||||||
_nameLabel.text = @"";
|
_nameLabel.text = @"";
|
||||||
_durationLabel.text = @"";
|
_durationLabel.text = @"";
|
||||||
_protocolLabel.text = @"";
|
_protocolLabel.text = @"";
|
||||||
|
_encryptIcon.hidden = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// 视频编码器(H.264/H.265)
|
/// 视频编码器(H.264/H.265)
|
||||||
@property (nonatomic, assign) AVVideoCodec codec;
|
@property (nonatomic, assign) AVVideoCodec codec;
|
||||||
|
|
||||||
|
/// XOR 加密密钥(十六进制字符串,如 @"AABBCCDD"),为空则不加密
|
||||||
|
@property (nonatomic, copy, nullable) NSString *xorKey;
|
||||||
|
|
||||||
// ============ 工厂方法 ============
|
// ============ 工厂方法 ============
|
||||||
|
|
||||||
/// 创建默认配置
|
/// 创建默认配置
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
#import "TokenGenerator.h"
|
#import "TokenGenerator.h"
|
||||||
#import "AVLiveStreamModel.h"
|
#import "AVLiveStreamModel.h"
|
||||||
#import "AVConstants.h"
|
#import "AVConstants.h"
|
||||||
#import <AFNetworking/AFNetworking.h>
|
#import "AVApiService.h"
|
||||||
#import <YYModel/YYModel.h>
|
|
||||||
|
|
||||||
@interface SCLivePusherViewController ()<SellyLivePusherDelegate, SellyLivePlayerDelegate>
|
@interface SCLivePusherViewController ()<SellyLivePusherDelegate, SellyLivePlayerDelegate>
|
||||||
@property (nonatomic, strong)UIView *liveView;
|
@property (nonatomic, strong)UIView *liveView;
|
||||||
@@ -196,6 +195,16 @@
|
|||||||
[AVConfigManager.sharedManager saveConfig];
|
[AVConfigManager.sharedManager saveConfig];
|
||||||
|
|
||||||
if (!self.isLiveStarted) {
|
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 startLive];
|
||||||
self.isLiveStarted = YES;
|
self.isLiveStarted = YES;
|
||||||
@@ -449,7 +458,8 @@
|
|||||||
|
|
||||||
NSString *token = [TokenGenerator generateStreamSignatureWithVhost:V_HOST appId:APP_ID channelId:self.videoConfig.streamId type:@"push" key:APP_SECRET];
|
NSString *token = [TokenGenerator generateStreamSignatureWithVhost:V_HOST appId:APP_ID channelId:self.videoConfig.streamId type:@"push" key:APP_SECRET];
|
||||||
self.livePusher.token = token;
|
self.livePusher.token = token;
|
||||||
|
self.livePusher.xorKey = self.videoConfig.xorKey;
|
||||||
|
|
||||||
NSError *error;
|
NSError *error;
|
||||||
if (self.videoConfig.streamId) {
|
if (self.videoConfig.streamId) {
|
||||||
error = [self.livePusher startLiveWithStreamId:self.videoConfig.streamId];
|
error = [self.livePusher startLiveWithStreamId:self.videoConfig.streamId];
|
||||||
@@ -460,6 +470,31 @@
|
|||||||
if (error) {
|
if (error) {
|
||||||
NSLog(@"###startLive failed. error == %@",error.localizedDescription);
|
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 {
|
- (void)setupItemContainer {
|
||||||
@@ -724,47 +759,27 @@
|
|||||||
[self presentViewController:loadingAlert animated:YES completion:nil];
|
[self presentViewController:loadingAlert animated:YES completion:nil];
|
||||||
|
|
||||||
// 获取直播列表
|
// 获取直播列表
|
||||||
AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
|
|
||||||
sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
|
|
||||||
sessionManager.requestSerializer.timeoutInterval = 10;
|
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
NSString *apiURL = @"http://rtmp.sellycloud.io:8089/live/sdk/alive-list";
|
[[AVApiService shared] fetchLiveStreams:^(NSArray<AVLiveStreamModel *> *streams) {
|
||||||
|
|
||||||
[sessionManager GET:apiURL parameters:nil headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *responseObject) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||||
if (!strongSelf) return;
|
if (!strongSelf) return;
|
||||||
|
|
||||||
// 关闭加载提示
|
|
||||||
[loadingAlert dismissViewControllerAnimated:YES completion:^{
|
[loadingAlert dismissViewControllerAnimated:YES completion:^{
|
||||||
// 解析数据
|
if (streams.count == 0) {
|
||||||
NSArray<AVLiveStreamModel *> *liveStreams = [NSArray yy_modelArrayWithClass:AVLiveStreamModel.class json:responseObject[@"list"]];
|
|
||||||
|
|
||||||
NSLog(@"✅ 获取 %ld 个直播流", (long)liveStreams.count);
|
|
||||||
|
|
||||||
if (liveStreams.count == 0) {
|
|
||||||
// 没有直播流
|
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
|
||||||
message:@"暂无正在直播的用户"
|
message:@"暂无正在直播的用户"
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||||
[strongSelf presentViewController:alert animated:YES completion:nil];
|
[strongSelf presentViewController:alert animated:YES completion:nil];
|
||||||
} else {
|
} else {
|
||||||
// 显示直播流列表
|
[strongSelf showStreamListWithStreams:streams];
|
||||||
[strongSelf showStreamListWithStreams:liveStreams];
|
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
} failure:^(NSError *error) {
|
||||||
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
|
|
||||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||||
if (!strongSelf) return;
|
if (!strongSelf) return;
|
||||||
|
|
||||||
// 关闭加载提示
|
|
||||||
[loadingAlert dismissViewControllerAnimated:YES completion:^{
|
[loadingAlert dismissViewControllerAnimated:YES completion:^{
|
||||||
NSLog(@"❌ 获取直播列表失败: %@", error.localizedDescription);
|
|
||||||
|
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
|
||||||
message:[NSString stringWithFormat:@"无法获取直播列表: %@", error.localizedDescription]
|
message:error.localizedDescription
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
|
||||||
[strongSelf presentViewController:alert animated:YES completion:nil];
|
[strongSelf presentViewController:alert animated:YES completion:nil];
|
||||||
|
|||||||
39
Example/SellyCloudSDK/Network/AVApiService.h
Normal file
39
Example/SellyCloudSDK/Network/AVApiService.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// AVApiService.h
|
||||||
|
// SellyCloudSDK_Example
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class AVLiveStreamModel;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef void(^AVApiSuccess)(id _Nullable responseObject);
|
||||||
|
typedef void(^AVApiFailure)(NSError *error);
|
||||||
|
|
||||||
|
@interface AVApiService : NSObject
|
||||||
|
|
||||||
|
+ (instancetype)shared;
|
||||||
|
|
||||||
|
/// GET /live/sdk/alive-list
|
||||||
|
- (void)fetchLiveStreams:(void(^)(NSArray<AVLiveStreamModel *> *streams))success
|
||||||
|
failure:(nullable AVApiFailure)failure;
|
||||||
|
|
||||||
|
/// POST /live/sdk/demo/stream-xor
|
||||||
|
- (void)reportXorKeyWithVhost:(NSString *)vhost
|
||||||
|
app:(NSString *)app
|
||||||
|
stream:(NSString *)stream
|
||||||
|
xorKey:(nullable NSString *)xorKey
|
||||||
|
success:(nullable AVApiSuccess)success
|
||||||
|
failure:(nullable AVApiFailure)failure;
|
||||||
|
|
||||||
|
/// POST /live/sdk/demo/login
|
||||||
|
- (void)loginWithUsername:(NSString *)username
|
||||||
|
password:(NSString *)password
|
||||||
|
success:(void(^)(id responseObject))success
|
||||||
|
failure:(void(^)(NSError *error, NSString * _Nullable serverMessage))failure;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
101
Example/SellyCloudSDK/Network/AVApiService.m
Normal file
101
Example/SellyCloudSDK/Network/AVApiService.m
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// AVApiService.m
|
||||||
|
// SellyCloudSDK_Example
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "AVApiService.h"
|
||||||
|
#import "AVLiveStreamModel.h"
|
||||||
|
#import <AFNetworking/AFNetworking.h>
|
||||||
|
#import <YYModel/YYModel.h>
|
||||||
|
|
||||||
|
static NSString * const kBaseURL = @"http://rtmp.sellycloud.io:8089";
|
||||||
|
|
||||||
|
@interface AVApiService ()
|
||||||
|
@property (nonatomic, strong) AFHTTPSessionManager *manager;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AVApiService
|
||||||
|
|
||||||
|
+ (instancetype)shared {
|
||||||
|
static AVApiService *instance;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
instance = [[AVApiService alloc] init];
|
||||||
|
});
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_manager = [AFHTTPSessionManager manager];
|
||||||
|
_manager.requestSerializer = [AFJSONRequestSerializer serializer];
|
||||||
|
_manager.responseSerializer = [AFJSONResponseSerializer serializer];
|
||||||
|
_manager.requestSerializer.timeoutInterval = 10;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchLiveStreams:(void(^)(NSArray<AVLiveStreamModel *> *streams))success
|
||||||
|
failure:(nullable AVApiFailure)failure {
|
||||||
|
NSString *url = [NSString stringWithFormat:@"%@/live/sdk/alive-list", kBaseURL];
|
||||||
|
[self.manager GET:url parameters:nil headers:nil progress:nil
|
||||||
|
success:^(NSURLSessionDataTask *task, id responseObject) {
|
||||||
|
NSLog(@"%@",responseObject);
|
||||||
|
NSArray *streams = [NSArray yy_modelArrayWithClass:AVLiveStreamModel.class json:responseObject[@"list"]];
|
||||||
|
if (success) success(streams ?: @[]);
|
||||||
|
} failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||||
|
NSLog(@"[AVApiService] fetchLiveStreams failed: %@", error.localizedDescription);
|
||||||
|
if (failure) failure(error);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)reportXorKeyWithVhost:(NSString *)vhost
|
||||||
|
app:(NSString *)app
|
||||||
|
stream:(NSString *)stream
|
||||||
|
xorKey:(nullable NSString *)xorKey
|
||||||
|
success:(nullable AVApiSuccess)success
|
||||||
|
failure:(nullable AVApiFailure)failure {
|
||||||
|
NSDictionary *params = @{
|
||||||
|
@"vhost": vhost ?: @"",
|
||||||
|
@"app": app ?: @"",
|
||||||
|
@"stream": stream ?: @"",
|
||||||
|
@"xor_key": xorKey ?: @""
|
||||||
|
};
|
||||||
|
NSString *url = [NSString stringWithFormat:@"%@/live/sdk/demo/stream-xor", kBaseURL];
|
||||||
|
[self.manager POST:url parameters:params headers:nil progress:nil
|
||||||
|
success:^(NSURLSessionDataTask *task, id responseObject) {
|
||||||
|
NSLog(@"[AVApiService] reportXorKey success: %@", responseObject);
|
||||||
|
if (success) success(responseObject);
|
||||||
|
} failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||||
|
NSLog(@"[AVApiService] reportXorKey failed: %@", error.localizedDescription);
|
||||||
|
if (failure) failure(error);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loginWithUsername:(NSString *)username
|
||||||
|
password:(NSString *)password
|
||||||
|
success:(void(^)(id responseObject))success
|
||||||
|
failure:(void(^)(NSError *error, NSString * _Nullable serverMessage))failure {
|
||||||
|
NSDictionary *params = @{
|
||||||
|
@"username": username ?: @"",
|
||||||
|
@"password": password ?: @""
|
||||||
|
};
|
||||||
|
NSString *url = [NSString stringWithFormat:@"%@/live/sdk/demo/login", kBaseURL];
|
||||||
|
[self.manager POST:url parameters:params headers:nil progress:nil
|
||||||
|
success:^(NSURLSessionDataTask *task, id responseObject) {
|
||||||
|
NSLog(@"[AVApiService] login success");
|
||||||
|
if (success) success(responseObject);
|
||||||
|
} failure:^(NSURLSessionDataTask *task, NSError *error) {
|
||||||
|
NSLog(@"[AVApiService] login failed: %@", error.localizedDescription);
|
||||||
|
NSString *serverMessage = nil;
|
||||||
|
NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
|
||||||
|
if (errorData) {
|
||||||
|
NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData options:0 error:nil];
|
||||||
|
serverMessage = errorDict[@"message"] ?: errorDict[@"error"] ?: errorDict[@"msg"];
|
||||||
|
}
|
||||||
|
if (failure) failure(error, serverMessage);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic, assign) NSTimeInterval duration;
|
@property (nonatomic, assign) NSTimeInterval duration;
|
||||||
@property (nonatomic, copy) NSString *preview_image; // 预览图 URL
|
@property (nonatomic, copy) NSString *preview_image; // 预览图 URL
|
||||||
@property (nonatomic, copy) NSString *play_protocol; // 播放协议:rtc, rtmp, flv, hls 等
|
@property (nonatomic, copy) NSString *play_protocol; // 播放协议:rtc, rtmp, flv, hls 等
|
||||||
|
@property (nonatomic, copy, nullable) NSString *xorKey; // XOR 解密密钥 (十六进制字符串)
|
||||||
|
|
||||||
- (NSString *)displayName;
|
- (NSString *)displayName;
|
||||||
- (NSString *)durationString;
|
- (NSString *)durationString;
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
|
|
||||||
@implementation AVLiveStreamModel
|
@implementation AVLiveStreamModel
|
||||||
|
|
||||||
|
+ (NSDictionary *)modelCustomPropertyMapper {
|
||||||
|
return @{
|
||||||
|
@"xorKey" : @"xor_key"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString *)displayName {
|
- (NSString *)displayName {
|
||||||
return self.stream.length > 0 ? self.stream : @"未知流";
|
return self.stream.length > 0 ? self.stream : @"未知流";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,6 +387,7 @@
|
|||||||
NSString *token = [TokenGenerator generateStreamSignatureWithVhost:self.streamModel.vhost appId:self.streamModel.app channelId:config.streamId type:@"pull" key:APP_SECRET];
|
NSString *token = [TokenGenerator generateStreamSignatureWithVhost:self.streamModel.vhost appId:self.streamModel.app channelId:config.streamId type:@"pull" key:APP_SECRET];
|
||||||
SellyLiveVideoPlayer *player = [[SellyLiveVideoPlayer alloc] init];
|
SellyLiveVideoPlayer *player = [[SellyLiveVideoPlayer alloc] init];
|
||||||
player.token = token;
|
player.token = token;
|
||||||
|
player.xorKey = self.streamModel.xorKey ?: config.xorKey;
|
||||||
player.delegate = self;
|
player.delegate = self;
|
||||||
player.scaleMode = SellyPlayerScalingModeAspectFit;
|
player.scaleMode = SellyPlayerScalingModeAspectFit;
|
||||||
[player setRenderView:self.playerContainerView];
|
[player setRenderView:self.playerContainerView];
|
||||||
@@ -405,6 +406,7 @@
|
|||||||
NSString *pkToken = [TokenGenerator generateStreamSignatureWithVhost:self.streamModel.vhost appId:self.streamModel.app channelId:self.pkConfig.streamId type:@"pull" key:APP_SECRET];
|
NSString *pkToken = [TokenGenerator generateStreamSignatureWithVhost:self.streamModel.vhost appId:self.streamModel.app channelId:self.pkConfig.streamId type:@"pull" key:APP_SECRET];
|
||||||
SellyLiveVideoPlayer *pkPlayer = [[SellyLiveVideoPlayer alloc] init];
|
SellyLiveVideoPlayer *pkPlayer = [[SellyLiveVideoPlayer alloc] init];
|
||||||
pkPlayer.token = pkToken;
|
pkPlayer.token = pkToken;
|
||||||
|
pkPlayer.xorKey = self.streamModel.xorKey ?: self.currentConfig.xorKey;
|
||||||
pkPlayer.delegate = self;
|
pkPlayer.delegate = self;
|
||||||
pkPlayer.scaleMode = SellyPlayerScalingModeAspectFit;
|
pkPlayer.scaleMode = SellyPlayerScalingModeAspectFit;
|
||||||
[pkPlayer setRenderView:self.pkPlayerContainerView];
|
[pkPlayer setRenderView:self.pkPlayerContainerView];
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@interface SCPlayerConfig : NSObject
|
@interface SCPlayerConfig : NSObject
|
||||||
@property (nonatomic, assign) SellyLiveMode protocol;
|
@property (nonatomic, assign) SellyLiveMode protocol;
|
||||||
@property (nonatomic, strong) NSString *streamId;
|
@property (nonatomic, strong) NSString *streamId;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *xorKey;
|
||||||
|
|
||||||
/// 保存配置到 UserDefaults
|
/// 保存配置到 UserDefaults
|
||||||
- (void)saveToUserDefaults;
|
- (void)saveToUserDefaults;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// 配置保存的 key
|
// 配置保存的 key
|
||||||
static NSString * const kSCPlayerConfigProtocol = @"SCPlayerConfigProtocol";
|
static NSString * const kSCPlayerConfigProtocol = @"SCPlayerConfigProtocol";
|
||||||
static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
||||||
|
static NSString * const kSCPlayerConfigXorKey = @"SCPlayerConfigXorKey";
|
||||||
|
|
||||||
@implementation SCPlayerConfig
|
@implementation SCPlayerConfig
|
||||||
|
|
||||||
@@ -21,6 +22,11 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
if (self.streamId) {
|
if (self.streamId) {
|
||||||
[defaults setObject:self.streamId forKey:kSCPlayerConfigStreamId];
|
[defaults setObject:self.streamId forKey:kSCPlayerConfigStreamId];
|
||||||
}
|
}
|
||||||
|
if (self.xorKey.length > 0) {
|
||||||
|
[defaults setObject:self.xorKey forKey:kSCPlayerConfigXorKey];
|
||||||
|
} else {
|
||||||
|
[defaults removeObjectForKey:kSCPlayerConfigXorKey];
|
||||||
|
}
|
||||||
[defaults synchronize];
|
[defaults synchronize];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +45,10 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
// 加载 streamId
|
// 加载 streamId
|
||||||
NSString *streamId = [defaults objectForKey:kSCPlayerConfigStreamId];
|
NSString *streamId = [defaults objectForKey:kSCPlayerConfigStreamId];
|
||||||
config.streamId = streamId ? streamId : @"test";
|
config.streamId = streamId ? streamId : @"test";
|
||||||
|
|
||||||
|
// 加载 xorKey
|
||||||
|
config.xorKey = [defaults objectForKey:kSCPlayerConfigXorKey];
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +59,7 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
@property (nonatomic, strong) UIVisualEffectView *backgroundView;
|
@property (nonatomic, strong) UIVisualEffectView *backgroundView;
|
||||||
@property (nonatomic, strong) UISegmentedControl *protocolSegment;
|
@property (nonatomic, strong) UISegmentedControl *protocolSegment;
|
||||||
@property (nonatomic, strong) UITextField *streamIdField;
|
@property (nonatomic, strong) UITextField *streamIdField;
|
||||||
|
@property (nonatomic, strong) UITextField *xorKeyField;
|
||||||
@property (nonatomic, strong) UIButton *playButton;
|
@property (nonatomic, strong) UIButton *playButton;
|
||||||
@property (nonatomic, copy) void(^callback)(SCPlayerConfig *config);
|
@property (nonatomic, copy) void(^callback)(SCPlayerConfig *config);
|
||||||
@property (nonatomic, strong) MASConstraint *backgroundViewCenterYConstraint; // 保存约束引用
|
@property (nonatomic, strong) MASConstraint *backgroundViewCenterYConstraint; // 保存约束引用
|
||||||
@@ -172,6 +182,35 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
make.height.offset(44);
|
make.height.offset(44);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// 加密密钥标签
|
||||||
|
UILabel *xorKeyLabel = [[UILabel alloc] init];
|
||||||
|
xorKeyLabel.text = @"加密密钥 (Hex),留空不解密";
|
||||||
|
xorKeyLabel.font = [UIFont systemFontOfSize:14];
|
||||||
|
xorKeyLabel.numberOfLines = 0;
|
||||||
|
[_contentView addSubview:xorKeyLabel];
|
||||||
|
[xorKeyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(_streamIdField.mas_bottom).offset(24);
|
||||||
|
make.left.right.equalTo(_contentView);
|
||||||
|
make.height.offset(20);
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 加密密钥输入框
|
||||||
|
_xorKeyField = [[UITextField alloc] init];
|
||||||
|
_xorKeyField.placeholder = @"如 AABBCCDD";
|
||||||
|
_xorKeyField.borderStyle = UITextBorderStyleRoundedRect;
|
||||||
|
_xorKeyField.font = [UIFont systemFontOfSize:15];
|
||||||
|
_xorKeyField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||||||
|
_xorKeyField.returnKeyType = UIReturnKeyDone;
|
||||||
|
_xorKeyField.delegate = self;
|
||||||
|
_xorKeyField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||||
|
_xorKeyField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||||
|
[_contentView addSubview:_xorKeyField];
|
||||||
|
[_xorKeyField mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
|
make.top.equalTo(xorKeyLabel.mas_bottom).offset(8);
|
||||||
|
make.left.right.equalTo(_contentView);
|
||||||
|
make.height.offset(44);
|
||||||
|
}];
|
||||||
|
|
||||||
// 播放按钮
|
// 播放按钮
|
||||||
_playButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
_playButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
[_playButton setTitle:@"开始播放" forState:UIControlStateNormal];
|
[_playButton setTitle:@"开始播放" forState:UIControlStateNormal];
|
||||||
@@ -183,7 +222,7 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
[_playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
[_playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||||
[_contentView addSubview:_playButton];
|
[_contentView addSubview:_playButton];
|
||||||
[_playButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
[_playButton mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.top.equalTo(_streamIdField.mas_bottom).offset(32);
|
make.top.equalTo(_xorKeyField.mas_bottom).offset(32);
|
||||||
make.left.right.equalTo(_contentView);
|
make.left.right.equalTo(_contentView);
|
||||||
make.height.offset(50);
|
make.height.offset(50);
|
||||||
make.bottom.equalTo(_contentView);
|
make.bottom.equalTo(_contentView);
|
||||||
@@ -201,10 +240,24 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString *xorKey = [_xorKeyField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if (xorKey.length > 0) {
|
||||||
|
if (xorKey.length % 2 != 0) {
|
||||||
|
[self showAlertWithMessage:@"加密密钥必须为偶数长度"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSCharacterSet *hexChars = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];
|
||||||
|
if ([xorKey rangeOfCharacterFromSet:hexChars.invertedSet].location != NSNotFound) {
|
||||||
|
[self showAlertWithMessage:@"加密密钥只能包含十六进制字符 (0-9, a-f, A-F)"];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
|
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
|
||||||
config.protocol = _protocolSegment.selectedSegmentIndex == 0 ? SellyLiveMode_RTMP : SellyLiveMode_RTC;
|
config.protocol = _protocolSegment.selectedSegmentIndex == 0 ? SellyLiveMode_RTMP : SellyLiveMode_RTC;
|
||||||
config.streamId = streamId;
|
config.streamId = streamId;
|
||||||
|
config.xorKey = xorKey.length > 0 ? xorKey : nil;
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
[config saveToUserDefaults];
|
[config saveToUserDefaults];
|
||||||
|
|
||||||
@@ -306,6 +359,9 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
if (savedConfig.streamId && savedConfig.streamId.length > 0) {
|
if (savedConfig.streamId && savedConfig.streamId.length > 0) {
|
||||||
_streamIdField.text = savedConfig.streamId;
|
_streamIdField.text = savedConfig.streamId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置 xorKey
|
||||||
|
_xorKeyField.text = savedConfig.xorKey ?: @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - UITextFieldDelegate
|
#pragma mark - UITextFieldDelegate
|
||||||
@@ -340,8 +396,9 @@ static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
|
|||||||
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
||||||
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
|
||||||
|
|
||||||
// 计算输入框在屏幕上的位置
|
// 计算当前激活输入框在屏幕上的位置
|
||||||
CGRect textFieldFrame = [_streamIdField convertRect:_streamIdField.bounds toView:self];
|
UITextField *activeField = _xorKeyField.isFirstResponder ? _xorKeyField : _streamIdField;
|
||||||
|
CGRect textFieldFrame = [activeField convertRect:activeField.bounds toView:self];
|
||||||
CGFloat textFieldBottom = CGRectGetMaxY(textFieldFrame);
|
CGFloat textFieldBottom = CGRectGetMaxY(textFieldFrame);
|
||||||
|
|
||||||
// 计算需要的偏移量
|
// 计算需要的偏移量
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ typedef NS_OPTIONS(NSUInteger, AVSettingsFieldMask) {
|
|||||||
AVSettingsFieldStreamId = 1 << 0, // Show Stream ID field
|
AVSettingsFieldStreamId = 1 << 0, // Show Stream ID field
|
||||||
AVSettingsFieldNickname = 1 << 1, // Show Nickname field
|
AVSettingsFieldNickname = 1 << 1, // Show Nickname field
|
||||||
AVSettingsFieldVideoParams = 1 << 2, // Show video params (codec, resolution, fps, bitrate)
|
AVSettingsFieldVideoParams = 1 << 2, // Show video params (codec, resolution, fps, bitrate)
|
||||||
|
AVSettingsFieldXorKey = 1 << 3, // Show XOR encryption key field
|
||||||
|
|
||||||
// Convenient combinations
|
// Convenient combinations
|
||||||
AVSettingsFieldBasicPull = AVSettingsFieldStreamId | AVSettingsFieldNickname, // For pull page
|
AVSettingsFieldBasicPull = AVSettingsFieldStreamId | AVSettingsFieldNickname, // For pull page
|
||||||
AVSettingsFieldAll = AVSettingsFieldStreamId | AVSettingsFieldNickname | AVSettingsFieldVideoParams // For push page
|
AVSettingsFieldAll = AVSettingsFieldStreamId | AVSettingsFieldNickname | AVSettingsFieldVideoParams | AVSettingsFieldXorKey // For push page
|
||||||
};
|
};
|
||||||
|
|
||||||
@interface AVSettingsView : UIView
|
@interface AVSettingsView : UIView
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
@property (nonatomic, strong) UITextField *fpsField;
|
@property (nonatomic, strong) UITextField *fpsField;
|
||||||
@property (nonatomic, strong) UITextField *maxBitrateField;
|
@property (nonatomic, strong) UITextField *maxBitrateField;
|
||||||
@property (nonatomic, strong) UITextField *minBitrateField;
|
@property (nonatomic, strong) UITextField *minBitrateField;
|
||||||
|
@property (nonatomic, strong) UITextField *xorKeyField;
|
||||||
|
|
||||||
@property (nonatomic, strong) NSLayoutConstraint *containerCenterYConstraint;
|
@property (nonatomic, strong) NSLayoutConstraint *containerCenterYConstraint;
|
||||||
@property (nonatomic, weak) UITextField *activeTextField; // 当前激活的输入框
|
@property (nonatomic, weak) UITextField *activeTextField; // 当前激活的输入框
|
||||||
@@ -194,6 +195,37 @@
|
|||||||
topOffset += 40 + spacing;
|
topOffset += 40 + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XOR Key (after nickname, before video params)
|
||||||
|
if (self.fieldsMask & AVSettingsFieldXorKey) {
|
||||||
|
UILabel *xorKeyLabel = [[UILabel alloc] init];
|
||||||
|
xorKeyLabel.text = @"加密密钥 (Hex)";
|
||||||
|
xorKeyLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||||
|
[contentView addSubview:xorKeyLabel];
|
||||||
|
xorKeyLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[xorKeyLabel.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20],
|
||||||
|
[xorKeyLabel.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset]
|
||||||
|
]];
|
||||||
|
topOffset += 20;
|
||||||
|
|
||||||
|
_xorKeyField = [[UITextField alloc] init];
|
||||||
|
_xorKeyField.placeholder = @"如 AABBCCDD(留空不加密)";
|
||||||
|
_xorKeyField.borderStyle = UITextBorderStyleRoundedRect;
|
||||||
|
_xorKeyField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||||
|
_xorKeyField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||||
|
_xorKeyField.delegate = self;
|
||||||
|
_xorKeyField.returnKeyType = UIReturnKeyDone;
|
||||||
|
[contentView addSubview:_xorKeyField];
|
||||||
|
_xorKeyField.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[_xorKeyField.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor constant:20],
|
||||||
|
[_xorKeyField.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor constant:-20],
|
||||||
|
[_xorKeyField.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:topOffset],
|
||||||
|
[_xorKeyField.heightAnchor constraintEqualToConstant:40]
|
||||||
|
]];
|
||||||
|
topOffset += 40 + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
// Video parameters (conditional)
|
// Video parameters (conditional)
|
||||||
if (self.fieldsMask & AVSettingsFieldVideoParams) {
|
if (self.fieldsMask & AVSettingsFieldVideoParams) {
|
||||||
// Resolution
|
// Resolution
|
||||||
@@ -373,6 +405,14 @@
|
|||||||
if (_minBitrateField) {
|
if (_minBitrateField) {
|
||||||
_minBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoMinBitRate / 1000)];
|
_minBitrateField.text = [NSString stringWithFormat:@"%ld", (long)(config.videoMinBitRate / 1000)];
|
||||||
}
|
}
|
||||||
|
if (_xorKeyField) {
|
||||||
|
// Load from config first, fallback to cached value
|
||||||
|
NSString *key = config.xorKey;
|
||||||
|
if (key.length == 0) {
|
||||||
|
key = [[NSUserDefaults standardUserDefaults] stringForKey:@"selly_xor_key_cache"];
|
||||||
|
}
|
||||||
|
_xorKeyField.text = key ?: @"";
|
||||||
|
}
|
||||||
|
|
||||||
self.frame = viewController.view.bounds;
|
self.frame = viewController.view.bounds;
|
||||||
self.alpha = 0;
|
self.alpha = 0;
|
||||||
@@ -428,6 +468,17 @@
|
|||||||
self.tempConfig.videoMinBitRate = minBitrate * 1000; // Convert kbps to bps
|
self.tempConfig.videoMinBitRate = minBitrate * 1000; // Convert kbps to bps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_xorKeyField) {
|
||||||
|
NSString *key = [_xorKeyField.text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
|
||||||
|
self.tempConfig.xorKey = key.length > 0 ? key : nil;
|
||||||
|
// Cache for next time
|
||||||
|
if (key.length > 0) {
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:key forKey:@"selly_xor_key_cache"];
|
||||||
|
} else {
|
||||||
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"selly_xor_key_cache"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (self.callback) {
|
if (self.callback) {
|
||||||
self.callback(self.tempConfig);
|
self.callback(self.tempConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic, assign)SellyPlayerScalingMode scaleMode;
|
@property (nonatomic, assign)SellyPlayerScalingMode scaleMode;
|
||||||
//token
|
//token
|
||||||
@property (nonatomic, strong)NSString *token;
|
@property (nonatomic, strong)NSString *token;
|
||||||
|
/// XOR 解密密钥 (十六进制字符串, 如 @"ABCDEF12")
|
||||||
|
/// 必须为偶数长度且仅包含 0-9, a-f, A-F,传入非法值将抛出 NSInvalidArgumentException 导致 App 崩溃。
|
||||||
|
/// 传 nil 或空字符串表示不解密。
|
||||||
|
@property (nonatomic, copy, nullable)NSString *xorKey;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic, assign)SellyLiveVideoConfiguration *videoConfig;
|
@property (nonatomic, assign)SellyLiveVideoConfiguration *videoConfig;
|
||||||
//token
|
//token
|
||||||
@property (nonatomic, strong)NSString *token;
|
@property (nonatomic, strong)NSString *token;
|
||||||
|
/// XOR 加密密钥 (十六进制字符串, 如 @"ABCDEF12")
|
||||||
|
/// 必须为偶数长度且仅包含 0-9, a-f, A-F,传入非法值将抛出 NSInvalidArgumentException 导致 App 崩溃。
|
||||||
|
/// 传 nil 或空字符串表示不加密。
|
||||||
|
@property (nonatomic, copy, nullable)NSString *xorKey;
|
||||||
|
|
||||||
/// 是否启用 Center Stage(人物居中),默认 NO
|
/// 是否启用 Center Stage(人物居中),默认 NO
|
||||||
/// 支持运行时动态切换,不支持的设备设置无效
|
/// 支持运行时动态切换,不支持的设备设置无效
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user