// // 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 #import #import "SCLiveItemContainerView.h" #import #import #import "TokenGenerator.h" #import "SCPlayerDebugView.h" #import #import "SellyCallPiPManager.h" // 🎯 导入画中画管理器 @interface SCLiveVideoPlayerViewController () @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