Initial clean commit

This commit is contained in:
2026-03-26 12:05:37 +08:00
commit 7c3c8dffee
1177 changed files with 147248 additions and 0 deletions

View File

@@ -0,0 +1,824 @@
//
// 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 "AVConfigManager.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 {
// 使 RTMPflvhls 使 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 {
//
AVConfigManager *avConfig = AVConfigManager.sharedManager;
if (avConfig.localServerEnabled) {
[SellyCloudManager.sharedInstance setLocalServerIP:avConfig.localServerIP];
[SellyCloudManager.sharedInstance setForcedNodeIP:nil];
} else {
[SellyCloudManager.sharedInstance setLocalServerIP:nil];
[SellyCloudManager.sharedInstance setForcedNodeIP:@"43.106.88.179:1080"];
}
//
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 {
NSLog(@"统计信息: 协议=%@, CPU=%ld%%/%ld%%, FPS=%ld, 码率=%ldkbps/%ldkbps, RTT=%ldms ,freezeTime=%ldms, 分辨率=%@, 丢包率=%.2f%% timestampMs=%lld",
stats.protocol,
stats.appCpu,
stats.systemCpu,
stats.fps,
stats.videoBitrate,
stats.audioBitrate,
stats.rtt,
stats.freezeTime,
NSStringFromCGSize(stats.videoSize),
stats.packetLossRate,
stats.timestampMs);
}
#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