Files
SellyCloudSDK_demo/Example/SellyCloudSDK/Play/SCLiveVideoPlayerViewController.m
2026-04-07 18:20:16 +08:00

805 lines
30 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 {
// 默认使用 RTMPflv、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.xorKey = self.streamModel.xorKey ?: config.xorKey;
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.xorKey = self.streamModel.xorKey ?: self.currentConfig.xorKey;
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