803 lines
29 KiB
Objective-C
803 lines
29 KiB
Objective-C
//
|
||
// SCVODPlayerViewController.m
|
||
// SellyCloudSDK_Example
|
||
//
|
||
// Created by Caleb on 16/9/25.
|
||
// Copyright © 2025 Caleb. All rights reserved.
|
||
//
|
||
|
||
#import "SCLiveVideoPlayerViewController.h"
|
||
#import "SCPlayerConfigView.h"
|
||
#import "AVLiveStreamModel.h"
|
||
#import "AVConstants.h"
|
||
#import <SellyCloudSDK/SellyCloudManager.h>
|
||
#import <Masonry/Masonry.h>
|
||
#import "SCLiveItemContainerView.h"
|
||
#import <Photos/Photos.h>
|
||
#import <MediaPlayer/MediaPlayer.h>
|
||
#import "TokenGenerator.h"
|
||
#import "SCPlayerDebugView.h"
|
||
#import <SDWebImage/SDWebImage.h>
|
||
#import "SellyCallPiPManager.h" // 🎯 导入画中画管理器
|
||
|
||
@interface SCLiveVideoPlayerViewController ()<SellyLivePlayerDelegate>
|
||
@property (nonatomic, strong) SellyLiveVideoPlayer *player;
|
||
@property (nonatomic, strong) UIView *playerContainerView;
|
||
@property (nonatomic, strong) SCLiveItemContainerView *containerView;
|
||
@property (nonatomic, strong) SCPlayerConfig *currentConfig;
|
||
@property (nonatomic, strong) UIButton *closeButton;
|
||
@property (nonatomic, strong) SCPlayerDebugView *debugView;
|
||
|
||
// 🎯 连麦 PK 模式相关
|
||
@property (nonatomic, assign) BOOL isPKMode; // 是否是连麦模式
|
||
@property (nonatomic, strong) SellyLiveVideoPlayer *pkPlayer; // PK 播放器
|
||
@property (nonatomic, strong) UIView *pkPlayerContainerView; // PK 播放器容器
|
||
@property (nonatomic, strong) SCPlayerConfig *pkConfig; // PK 播放器配置
|
||
|
||
// 封面相关
|
||
@property (nonatomic, strong) UIImageView *coverImageView;
|
||
@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator;
|
||
@property (nonatomic, strong) UIImageView *pkCoverImageView; // PK 播放器封面
|
||
@property (nonatomic, strong) UIActivityIndicatorView *pkLoadingIndicator; // PK 加载指示器
|
||
|
||
// 保存按钮 model 的引用,用于更新状态
|
||
@property (nonatomic, strong) SCLiveItemModel *playPauseModel;
|
||
@property (nonatomic, strong) SCLiveItemModel *muteModel;
|
||
@property (nonatomic, strong) SCLiveItemModel *pipModel; // 🎯 画中画按钮
|
||
@property (nonatomic, strong) AVLiveStreamModel *streamModel;
|
||
|
||
// 🎯 画中画管理器
|
||
@property (nonatomic, strong) SellyCallPiPManager *pipManager;
|
||
@end
|
||
|
||
@implementation SCLiveVideoPlayerViewController
|
||
|
||
- (instancetype)initWithLiveStream:(AVLiveStreamModel *)stream {
|
||
self = [super init];
|
||
self.streamModel = stream;
|
||
|
||
// 🎯 检测是否是连麦 PK 模式
|
||
self.isPKMode = (stream.stream_pk != nil && stream.stream_pk.length > 0);
|
||
|
||
if (self.isPKMode) {
|
||
NSLog(@"🎯 检测到连麦 PK 房间 - 主播: %@, PK: %@", stream.stream, stream.stream_pk);
|
||
}
|
||
|
||
[self convertStreamModelToPlayerConfig:stream];
|
||
return self;
|
||
}
|
||
|
||
- (void)viewDidLoad {
|
||
[super viewDidLoad];
|
||
self.view.backgroundColor = UIColor.blackColor;
|
||
|
||
// 隐藏导航栏
|
||
self.navigationController.navigationBarHidden = YES;
|
||
|
||
// 🎯 根据是否是 PK 模式,设置不同的布局
|
||
if (self.isPKMode) {
|
||
[self setupPKModeLayout];
|
||
} else {
|
||
[self setupNormalModeLayout];
|
||
}
|
||
|
||
// 添加封面图片(在播放器容器上方)
|
||
[self setupCoverImageView];
|
||
|
||
// 添加关闭按钮(右上角)
|
||
[self setupCloseButton];
|
||
|
||
// 添加调试视图(右上角,关闭按钮下方)
|
||
[self setupDebugView];
|
||
|
||
// 🎯 添加控制按钮容器(所有模式都显示)
|
||
[self.view addSubview:self.containerView];
|
||
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.right.offset(0);
|
||
make.bottom.offset(-48);
|
||
make.height.offset(100);
|
||
}];
|
||
|
||
// 禁止息屏
|
||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||
|
||
[self startPlayWithConfig:self.currentConfig];
|
||
}
|
||
|
||
- (void)convertStreamModelToPlayerConfig:(AVLiveStreamModel *)stream {
|
||
if (!stream) {
|
||
NSLog(@"❌ stream model is nil");
|
||
return;
|
||
}
|
||
|
||
// 创建主播放器配置
|
||
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
|
||
config.streamId = stream.stream;
|
||
|
||
// 根据 playProtocol 字符串设置协议类型
|
||
NSString *protocol = stream.play_protocol.lowercaseString;
|
||
if ([protocol isEqualToString:@"rtc"] || [protocol isEqualToString:@"webrtc"]) {
|
||
config.protocol = SellyLiveMode_RTC;
|
||
} else if ([protocol isEqualToString:@"rtmp"]) {
|
||
config.protocol = SellyLiveMode_RTMP;
|
||
} else {
|
||
// 默认使用 RTMP(flv、hls 等也使用 RTMP 模式)
|
||
config.protocol = SellyLiveMode_RTMP;
|
||
}
|
||
|
||
NSLog(@"🔄 转换 StreamModel -> PlayerConfig: stream=%@, protocol=%@ (%@)",
|
||
config.streamId,
|
||
stream.play_protocol,
|
||
config.protocol == SellyLiveMode_RTC ? @"RTC" : @"RTMP");
|
||
|
||
// 保存主播放器配置
|
||
self.currentConfig = config;
|
||
|
||
// 🎯 如果是 PK 模式,创建 PK 播放器配置
|
||
if (self.isPKMode) {
|
||
SCPlayerConfig *pkConfig = [[SCPlayerConfig alloc] init];
|
||
pkConfig.streamId = stream.stream_pk;
|
||
pkConfig.protocol = config.protocol; // 使用相同的协议
|
||
|
||
NSLog(@"🔄 创建 PK PlayerConfig: stream=%@, protocol=%@",
|
||
pkConfig.streamId,
|
||
pkConfig.protocol == SellyLiveMode_RTC ? @"RTC" : @"RTMP");
|
||
|
||
self.pkConfig = pkConfig;
|
||
}
|
||
}
|
||
|
||
#pragma mark - Layout Setup
|
||
|
||
// 🎯 普通模式布局(单播放器,全屏显示)
|
||
- (void)setupNormalModeLayout {
|
||
[self.view addSubview:self.playerContainerView];
|
||
[self.playerContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.view);
|
||
}];
|
||
}
|
||
|
||
// 🎯 PK 模式布局(双播放器,左右并排)
|
||
- (void)setupPKModeLayout {
|
||
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
|
||
CGFloat space = 8.0; // 间距
|
||
CGFloat playerWidth = (screenWidth - space) / 2.0; // 单个播放器宽度
|
||
CGFloat playerHeight = playerWidth * 16.0 / 9.0; // 宽高比 9:16
|
||
|
||
// 添加主播放器容器(左侧)
|
||
[self.view addSubview:self.playerContainerView];
|
||
[self.playerContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.left.equalTo(self.view);
|
||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
|
||
make.width.offset(playerWidth);
|
||
make.height.offset(playerHeight);
|
||
}];
|
||
|
||
// 添加 PK 播放器容器(右侧)
|
||
[self.view addSubview:self.pkPlayerContainerView];
|
||
[self.pkPlayerContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.right.equalTo(self.view);
|
||
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
|
||
make.width.offset(playerWidth);
|
||
make.height.offset(playerHeight);
|
||
}];
|
||
|
||
NSLog(@"🎯 PK 模式布局完成 - 播放器宽度: %.1f, 高度: %.1f, 间距: %.1f", playerWidth, playerHeight, space);
|
||
}
|
||
|
||
- (void)setupCloseButton {
|
||
_closeButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
[_closeButton setImage:[UIImage systemImageNamed:@"xmark.circle.fill"] forState:UIControlStateNormal];
|
||
_closeButton.tintColor = [UIColor whiteColor];
|
||
[_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);
|
||
}];
|
||
}
|
||
|
||
- (void)setupDebugView {
|
||
_debugView = [[SCPlayerDebugView alloc] init];
|
||
[self.view addSubview:_debugView];
|
||
[_debugView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.right.offset(-16);
|
||
make.top.equalTo(self.closeButton.mas_bottom).offset(16);
|
||
make.width.offset(300);
|
||
}];
|
||
|
||
// 添加初始日志
|
||
[_debugView appendLog:@"调试器已初始化" withPrefix:@"✅"];
|
||
}
|
||
|
||
- (void)setupCoverImageView {
|
||
// 创建主播放器封面图片视图
|
||
_coverImageView = [[UIImageView alloc] init];
|
||
_coverImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||
_coverImageView.backgroundColor = [UIColor blackColor];
|
||
[self.view addSubview:_coverImageView];
|
||
|
||
if (self.isPKMode) {
|
||
// 🎯 PK 模式:封面覆盖左侧播放器
|
||
[_coverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.playerContainerView);
|
||
}];
|
||
} else {
|
||
// 普通模式:封面全屏
|
||
[_coverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.view);
|
||
}];
|
||
}
|
||
|
||
// 创建主播放器加载指示器
|
||
_loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
|
||
_loadingIndicator.color = [UIColor whiteColor];
|
||
[self.view addSubview:_loadingIndicator];
|
||
[_loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.center.equalTo(self.coverImageView);
|
||
}];
|
||
[_loadingIndicator startAnimating];
|
||
|
||
// 🎯 如果是 PK 模式,创建 PK 播放器的封面和加载指示器
|
||
if (self.isPKMode) {
|
||
_pkCoverImageView = [[UIImageView alloc] init];
|
||
_pkCoverImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||
_pkCoverImageView.backgroundColor = [UIColor blackColor];
|
||
[self.view addSubview:_pkCoverImageView];
|
||
[_pkCoverImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.edges.equalTo(self.pkPlayerContainerView);
|
||
}];
|
||
|
||
_pkLoadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
|
||
_pkLoadingIndicator.color = [UIColor whiteColor];
|
||
[self.view addSubview:_pkLoadingIndicator];
|
||
[_pkLoadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) {
|
||
make.center.equalTo(self.pkCoverImageView);
|
||
}];
|
||
[_pkLoadingIndicator startAnimating];
|
||
}
|
||
|
||
// 加载主播放器封面图片
|
||
if (self.streamModel.preview_image && self.streamModel.preview_image.length > 0) {
|
||
[self loadCoverImage:self.streamModel.preview_image];
|
||
[self.debugView appendLog:@"正在加载主播放器封面..." withPrefix:@"🖼️"];
|
||
} else {
|
||
[self.debugView appendLog:@"主播放器无封面,等待视频首帧..." withPrefix:@"⏳"];
|
||
}
|
||
|
||
// 🎯 PK 模式下,PK 播放器也显示相同的封面(因为没有单独的 PK 封面数据)
|
||
if (self.isPKMode) {
|
||
if (self.streamModel.preview_image && self.streamModel.preview_image.length > 0) {
|
||
[self loadPKCoverImage:self.streamModel.preview_image];
|
||
[self.debugView appendLog:@"正在加载 PK 播放器封面..." withPrefix:@"🖼️"];
|
||
} else {
|
||
[self.debugView appendLog:@"PK 播放器无封面,等待视频首帧..." withPrefix:@"⏳"];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)loadCoverImage:(NSString *)imageUrlString {
|
||
NSURL *url = [NSURL URLWithString:imageUrlString];
|
||
if (!url) {
|
||
NSLog(@"❌ 无效的封面图片 URL: %@", imageUrlString);
|
||
[self.debugView appendLog:@"无效的封面 URL" withPrefix:@"❌"];
|
||
return;
|
||
}
|
||
|
||
// 使用 SDWebImage 加载图片,支持缓存和占位符
|
||
[self.coverImageView sd_setImageWithURL:[NSURL URLWithString:imageUrlString]];
|
||
}
|
||
|
||
// 🎯 加载 PK 播放器封面
|
||
- (void)loadPKCoverImage:(NSString *)imageUrlString {
|
||
NSURL *url = [NSURL URLWithString:imageUrlString];
|
||
if (!url) {
|
||
NSLog(@"❌ 无效的 PK 封面图片 URL: %@", imageUrlString);
|
||
[self.debugView appendLog:@"无效的 PK 封面 URL" withPrefix:@"❌"];
|
||
return;
|
||
}
|
||
|
||
[self.pkCoverImageView sd_setImageWithURL:[NSURL URLWithString:imageUrlString]];
|
||
}
|
||
|
||
- (void)hideCoverImage {
|
||
[UIView animateWithDuration:0.3 animations:^{
|
||
self.coverImageView.alpha = 0;
|
||
self.loadingIndicator.alpha = 0;
|
||
} completion:^(BOOL finished) {
|
||
[self.coverImageView removeFromSuperview];
|
||
self.coverImageView = nil;
|
||
[self.loadingIndicator stopAnimating];
|
||
[self.loadingIndicator removeFromSuperview];
|
||
self.loadingIndicator = nil;
|
||
}];
|
||
}
|
||
|
||
// 🎯 隐藏 PK 播放器封面
|
||
- (void)hidePKCoverImage {
|
||
[UIView animateWithDuration:0.3 animations:^{
|
||
self.pkCoverImageView.alpha = 0;
|
||
self.pkLoadingIndicator.alpha = 0;
|
||
} completion:^(BOOL finished) {
|
||
[self.pkCoverImageView removeFromSuperview];
|
||
self.pkCoverImageView = nil;
|
||
[self.pkLoadingIndicator stopAnimating];
|
||
[self.pkLoadingIndicator removeFromSuperview];
|
||
self.pkLoadingIndicator = nil;
|
||
}];
|
||
}
|
||
|
||
- (void)closeButtonTapped {
|
||
// 停止播放
|
||
if (self.player) {
|
||
[self.player stop];
|
||
}
|
||
// 🎯 PK 模式:停止 PK 播放器
|
||
if (self.isPKMode && self.pkPlayer) {
|
||
[self.pkPlayer stop];
|
||
}
|
||
|
||
// 恢复导航栏
|
||
self.navigationController.navigationBarHidden = NO;
|
||
|
||
// 返回上一页
|
||
[self.navigationController popViewControllerAnimated:YES];
|
||
}
|
||
|
||
- (void)viewDidLayoutSubviews {
|
||
[super viewDidLayoutSubviews];
|
||
// 确保封面在播放器上方,但在控制按钮、调试视图和关闭按钮下方
|
||
if (self.coverImageView) {
|
||
[self.view bringSubviewToFront:self.coverImageView];
|
||
[self.view bringSubviewToFront:self.loadingIndicator];
|
||
}
|
||
// 🎯 PK 模式:确保 PK 封面也在正确的层级
|
||
if (self.isPKMode && self.pkCoverImageView) {
|
||
[self.view bringSubviewToFront:self.pkCoverImageView];
|
||
[self.view bringSubviewToFront:self.pkLoadingIndicator];
|
||
}
|
||
[self.view bringSubviewToFront:self.containerView];
|
||
[self.view bringSubviewToFront:self.debugView];
|
||
[self.view bringSubviewToFront:self.closeButton];
|
||
}
|
||
|
||
- (void)showConfigView {
|
||
SCPlayerConfigView *configView = [[SCPlayerConfigView alloc] init];
|
||
__weak typeof(self) weakSelf = self;
|
||
[configView showInViewController:self callback:^(SCPlayerConfig *config) {
|
||
weakSelf.currentConfig = config;
|
||
[weakSelf startPlayWithConfig:config];
|
||
}];
|
||
|
||
// 配置视图现在添加到 self.view 上,确保关闭按钮在最上层
|
||
[self.view bringSubviewToFront:self.closeButton];
|
||
}
|
||
|
||
- (void)startPlayWithConfig:(SCPlayerConfig *)config {
|
||
// 如果已经有播放器在运行,先停止
|
||
if (self.player) {
|
||
[self.player stop];
|
||
[self.player setRenderView:nil];
|
||
self.player = nil;
|
||
}
|
||
self.currentConfig = config;
|
||
|
||
// 🎯 创建主播放器
|
||
NSString *token = [TokenGenerator generateStreamSignatureWithVhost:self.streamModel.vhost appId:self.streamModel.app channelId:config.streamId type:@"pull" key:APP_SECRET];
|
||
SellyLiveVideoPlayer *player = [[SellyLiveVideoPlayer alloc] init];
|
||
player.token = token;
|
||
player.delegate = self;
|
||
player.scaleMode = SellyPlayerScalingModeAspectFit;
|
||
[player setRenderView:self.playerContainerView];
|
||
|
||
// 确保初始音量为正常状态(未静音)
|
||
if (player.playbackVolume == 0) {
|
||
player.playbackVolume = 1.0;
|
||
}
|
||
self.player = player;
|
||
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"初始化主播放器: streamId=%@", config.streamId] withPrefix:@"🎬"];
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"协议: %@", config.protocol == SellyLiveMode_RTC ? @"RTC" : @"RTMP"] withPrefix:@"📡"];
|
||
|
||
// 🎯 如果是 PK 模式,创建 PK 播放器
|
||
if (self.isPKMode && self.pkConfig) {
|
||
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];
|
||
pkPlayer.token = pkToken;
|
||
pkPlayer.delegate = self;
|
||
pkPlayer.scaleMode = SellyPlayerScalingModeAspectFit;
|
||
[pkPlayer setRenderView:self.pkPlayerContainerView];
|
||
|
||
if (pkPlayer.playbackVolume == 0) {
|
||
pkPlayer.playbackVolume = 1.0;
|
||
}
|
||
self.pkPlayer = pkPlayer;
|
||
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"初始化 PK 播放器: streamId=%@", self.pkConfig.streamId] withPrefix:@"🎬"];
|
||
}
|
||
|
||
[self startPlay];
|
||
|
||
// 设置控制按钮
|
||
[self setupControlButtons];
|
||
}
|
||
|
||
- (void)startPlay {
|
||
// 判断是 URL 还是 streamId
|
||
SCPlayerConfig *config = self.currentConfig;
|
||
if ([config.streamId hasPrefix:@"rtmp://"] ||
|
||
[config.streamId hasPrefix:@"http://"] ||
|
||
[config.streamId hasPrefix:@"https://"]) {
|
||
// 完整 URL
|
||
[self.player startPlayUrl:config.streamId];
|
||
}
|
||
else if (self.streamModel.url) {
|
||
[self.player startPlayUrl:self.streamModel.url];
|
||
}
|
||
else {
|
||
// StreamId,需要构造 SellyPlayerStreamInfo
|
||
SellyPlayerStreamInfo *streamInfo = [[SellyPlayerStreamInfo alloc] init];
|
||
streamInfo.streamId = config.streamId;
|
||
streamInfo.protocol = config.protocol;
|
||
[self.player startPlayStreamInfo:streamInfo];
|
||
}
|
||
|
||
// 🎯 如果是 PK 模式,启动 PK 播放器
|
||
if (self.isPKMode && self.pkPlayer && self.pkConfig) {
|
||
SellyPlayerStreamInfo *pkStreamInfo = [[SellyPlayerStreamInfo alloc] init];
|
||
pkStreamInfo.streamId = self.pkConfig.streamId;
|
||
pkStreamInfo.protocol = self.pkConfig.protocol;
|
||
[self.pkPlayer startPlayStreamInfo:pkStreamInfo];
|
||
|
||
[self.debugView appendLog:@"PK 播放器已启动" withPrefix:@"▶️"];
|
||
}
|
||
}
|
||
|
||
- (void)setupControlButtons {
|
||
__weak typeof(self) weakSelf = self;
|
||
NSMutableArray *items = NSMutableArray.new;
|
||
|
||
// 播放/暂停按钮(所有模式都显示)
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = SCLiveItemTypePlayPause;
|
||
model.title = @"播放/暂停";
|
||
model.isSelected = NO;
|
||
model.clickCallback = ^{
|
||
if (weakSelf.player.isPlaying) {
|
||
[weakSelf pause];
|
||
} else {
|
||
[weakSelf start];
|
||
}
|
||
[weakSelf updatePlayPauseButtonState];
|
||
};
|
||
self.playPauseModel = model;
|
||
[items addObject:model];
|
||
}
|
||
|
||
// 静音按钮(所有模式都显示)
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = SCLiveItemTypeVolume;
|
||
model.title = @"静音";
|
||
|
||
// 获取当前音量状态
|
||
CGFloat currentVolume = self.player.playbackVolume;
|
||
model.isSelected = (currentVolume == 0);
|
||
|
||
NSLog(@"📢 静音按钮初始化 - 当前音量: %.2f, isSelected: %d", currentVolume, model.isSelected);
|
||
|
||
model.clickCallback = ^{
|
||
// 🎯 PK 模式:同时控制所有播放器的音量
|
||
if (weakSelf.isPKMode) {
|
||
BOOL shouldMute = (weakSelf.player.playbackVolume > 0);
|
||
weakSelf.player.playbackVolume = shouldMute ? 0 : 1.0;
|
||
weakSelf.pkPlayer.playbackVolume = shouldMute ? 0 : 1.0;
|
||
NSLog(@"📢 PK 模式 - 所有播放器音量: %.2f", weakSelf.player.playbackVolume);
|
||
} else {
|
||
weakSelf.player.playbackVolume = 1 - weakSelf.player.playbackVolume;
|
||
NSLog(@"📢 点击静音按钮 - 新音量: %.2f", weakSelf.player.playbackVolume);
|
||
}
|
||
[weakSelf updateMuteButtonState];
|
||
};
|
||
self.muteModel = model;
|
||
[items addObject:model];
|
||
}
|
||
|
||
// 🎯 以下按钮仅在非 PK 模式下显示
|
||
if (!self.isPKMode) {
|
||
// 截图按钮
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = SCLiveItemTypeScreenshot;
|
||
model.title = @"截图";
|
||
model.clickCallback = ^{
|
||
[weakSelf saveCurrentFrameToPhotoAlbum:weakSelf.player.getCurrentImage];
|
||
};
|
||
[items addObject:model];
|
||
}
|
||
|
||
// 画中画按钮
|
||
{
|
||
SCLiveItemModel *model = SCLiveItemModel.new;
|
||
model.type = SCLiveItemTypePiP;
|
||
model.title = @"画中画";
|
||
model.isSelected = NO;
|
||
model.clickCallback = ^{
|
||
[weakSelf togglePiP];
|
||
};
|
||
self.pipModel = model;
|
||
[items addObject:model];
|
||
}
|
||
}
|
||
|
||
self.containerView.models = items;
|
||
[self updatePlayPauseButtonState];
|
||
}
|
||
|
||
- (void)updatePlayPauseButtonState {
|
||
self.playPauseModel.isSelected = self.player.isPlaying;
|
||
// 刷新 UI
|
||
self.containerView.models = self.containerView.models;
|
||
}
|
||
|
||
- (void)updateMuteButtonState {
|
||
self.muteModel.isSelected = (self.player.playbackVolume == 0);
|
||
// 刷新 UI
|
||
self.containerView.models = self.containerView.models;
|
||
}
|
||
|
||
// 🎯 更新画中画按钮状态
|
||
- (void)updatePiPButtonState {
|
||
if (@available(iOS 15.0, *)) {
|
||
self.pipModel.isSelected = self.pipManager.pipActive;
|
||
} else {
|
||
self.pipModel.isSelected = NO;
|
||
}
|
||
// 刷新 UI
|
||
self.containerView.models = self.containerView.models;
|
||
}
|
||
|
||
// 🎯 切换画中画
|
||
- (void)togglePiP {
|
||
if (@available(iOS 15.0, *)) {
|
||
if (!self.pipManager) {
|
||
// 初始化画中画管理器
|
||
self.pipManager = [[SellyCallPiPManager alloc] initWithRenderView:self.playerContainerView];
|
||
[self.pipManager setupIfNeeded];
|
||
[self.debugView appendLog:@"画中画初始化完成" withPrefix:@"🖼️"];
|
||
}
|
||
|
||
if (self.pipManager.pipPossible) {
|
||
[self.pipManager togglePiP];
|
||
[self.debugView appendLog:self.pipManager.pipActive ? @"开启画中画" : @"关闭画中画" withPrefix:@"🖼️"];
|
||
// 延迟更新按钮状态,等待 PiP 状态变化
|
||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||
[self updatePiPButtonState];
|
||
});
|
||
} else {
|
||
[self.debugView appendLog:@"当前设备不支持画中画" withPrefix:@"❌"];
|
||
NSLog(@"当前设备不支持画中画");
|
||
}
|
||
} else {
|
||
[self.debugView appendLog:@"iOS 15 以上才支持自定义画中画" withPrefix:@"❌"];
|
||
NSLog(@"iOS 15 以上才支持自定义画中画");
|
||
}
|
||
}
|
||
|
||
#pragma mark - Player Control
|
||
|
||
- (NSString *)stringFromPlayerState:(SellyPlayerState)state {
|
||
switch (state) {
|
||
case SellyPlayerStateIdle:
|
||
return @"空闲 (Idle)";
|
||
case SellyPlayerStateConnecting:
|
||
return @"连接中 (Connecting)";
|
||
case SellyPlayerStatePlaying:
|
||
return @"播放中 (Playing)";
|
||
case SellyPlayerStatePaused:
|
||
return @"暂停 (Paused)";
|
||
case SellyPlayerStateStoppedOrEnded:
|
||
return @"已停止/结束 (Stopped/Ended)";
|
||
case SellyPlayerStateFailed:
|
||
return @"失败 (Failed)";
|
||
default:
|
||
return [NSString stringWithFormat:@"未知状态 (%ld)", (long)state];
|
||
}
|
||
}
|
||
|
||
- (void)start {
|
||
[self startPlay];
|
||
}
|
||
|
||
- (void)pause {
|
||
[self.player stop];
|
||
// 🎯 PK 模式:同时停止 PK 播放器
|
||
if (self.isPKMode && self.pkPlayer) {
|
||
[self.pkPlayer stop];
|
||
}
|
||
}
|
||
|
||
#pragma mark - SellyPlayerManagerDelegate
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player playbackDidFinished:(NSDictionary *)resultInfo {
|
||
NSLog(@"Playback finished: %@", resultInfo);
|
||
[self.debugView appendLog:@"播放已结束" withPrefix:@"⏹️"];
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player playbackStateChanged:(SellyPlayerState)state {
|
||
NSString *stateString = [self stringFromPlayerState:state];
|
||
NSLog(@"🎬 播放状态变更: %@ (rawValue: %ld)", stateString, (long)state);
|
||
|
||
// 添加到调试器
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"状态变更: %@", stateString] withPrefix:@"🎬"];
|
||
|
||
// 🎯 当播放状态为 Playing 时,初始化画中画管理器
|
||
if (state == SellyPlayerStatePlaying && !self.pipManager) {
|
||
if (@available(iOS 15.0, *)) {
|
||
self.pipManager = [[SellyCallPiPManager alloc] initWithRenderView:self.playerContainerView];
|
||
[self.pipManager setupIfNeeded];
|
||
[self.debugView appendLog:@"画中画已就绪" withPrefix:@"✅"];
|
||
}
|
||
}
|
||
|
||
// 更新播放/暂停按钮状态
|
||
[self updatePlayPauseButtonState];
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player onError:(NSError *)error {
|
||
NSLog(@"Player error: %@", error.localizedDescription);
|
||
|
||
// 添加到调试器
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"错误: %@", error.localizedDescription] withPrefix:@"🔴"];
|
||
|
||
// 显示错误提示
|
||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"播放错误"
|
||
message:error.localizedDescription
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
[alert addAction:[UIAlertAction actionWithTitle:@"重试" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||
// 如果有 streamModel 就重新播放,否则显示配置界面
|
||
if (self.currentConfig) {
|
||
[self startPlayWithConfig:self.currentConfig];
|
||
} else {
|
||
[self showConfigView];
|
||
}
|
||
}]];
|
||
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
|
||
[self closeButtonTapped];
|
||
}]];
|
||
[self presentViewController:alert animated:YES completion:nil];
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player firstRemoteVideoFrame:(NSInteger)elapse {
|
||
// 🎯 区分主播放器和 PK 播放器
|
||
if (player == self.player) {
|
||
NSLog(@"###主播放器视频首帧加载耗时 == %ldms", elapse);
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"主播放器视频首帧: %ldms", elapse] withPrefix:@"📹"];
|
||
[self hideCoverImage];
|
||
} else if (self.isPKMode && player == self.pkPlayer) {
|
||
NSLog(@"###PK 播放器视频首帧加载耗时 == %ldms", elapse);
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"PK 播放器视频首帧: %ldms", elapse] withPrefix:@"📹"];
|
||
[self hidePKCoverImage];
|
||
}
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player firstRemoteAudioFrame:(NSInteger)elapse {
|
||
NSLog(@"###语音首帧加载耗时 == %ldms",elapse);
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"音频首帧: %ldms", elapse] withPrefix:@"🔊"];
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player onFrameCatchingStart:(CGFloat)rate {
|
||
NSLog(@"###追帧开始");
|
||
[self.debugView appendLog:[NSString stringWithFormat:@"追帧开始 x%.1f倍",rate] withPrefix:@"⚡️"];
|
||
}
|
||
|
||
- (void)playerDidEndFrameCatching:(SellyLiveVideoPlayer *)player {
|
||
NSLog(@"###追帧结束");
|
||
[self.debugView appendLog:@"追帧结束" withPrefix:@"✅"];
|
||
}
|
||
|
||
//自定义渲染,实现画中画
|
||
- (BOOL)player:(SellyLiveVideoPlayer *)player onRenderVideoFrame:(SellyRTCVideoFrame *)videoFrame {
|
||
if (self.isPKMode) {
|
||
return true;
|
||
}
|
||
else {
|
||
// 🎯 将视频帧喂给画中画管理器
|
||
if (self.pipManager && videoFrame.pixelBuffer) {
|
||
[self.pipManager feedVideoFrame:videoFrame];
|
||
}
|
||
// 返回 true 表示 SDK 继续默认渲染到播放器视图
|
||
return false;
|
||
}
|
||
}
|
||
|
||
- (void)player:(SellyLiveVideoPlayer *)player onDebugInfo:(SellyLivePlayerStats *)stats {
|
||
|
||
}
|
||
|
||
#pragma mark - Screenshot
|
||
|
||
- (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(@"已保存当前视频帧至相册");
|
||
}
|
||
}
|
||
|
||
#pragma mark - Lazy Loading
|
||
|
||
- (UIView *)playerContainerView {
|
||
if (!_playerContainerView) {
|
||
_playerContainerView = [[UIView alloc] init];
|
||
_playerContainerView.backgroundColor = [UIColor blackColor];
|
||
}
|
||
return _playerContainerView;
|
||
}
|
||
|
||
// 🎯 PK 播放器容器
|
||
- (UIView *)pkPlayerContainerView {
|
||
if (!_pkPlayerContainerView) {
|
||
_pkPlayerContainerView = [[UIView alloc] init];
|
||
_pkPlayerContainerView.backgroundColor = [UIColor blackColor];
|
||
}
|
||
return _pkPlayerContainerView;
|
||
}
|
||
|
||
- (SCLiveItemContainerView *)containerView {
|
||
if (!_containerView) {
|
||
_containerView = [[SCLiveItemContainerView alloc] init];
|
||
}
|
||
return _containerView;
|
||
}
|
||
|
||
#pragma mark - Lifecycle
|
||
|
||
- (void)dealloc {
|
||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||
NSLog(@"###%@ dealloc", NSStringFromClass(self.class));
|
||
}
|
||
|
||
- (void)viewDidDisappear:(BOOL)animated {
|
||
[super viewDidDisappear:animated];
|
||
|
||
// 恢复导航栏
|
||
self.navigationController.navigationBarHidden = NO;
|
||
|
||
// 退出页面时彻底销毁 PiP,释放旧 controller,下次进入时重新创建
|
||
[self.pipManager invalidate];
|
||
self.pipManager = nil;
|
||
}
|
||
|
||
@end
|