initial commit

This commit is contained in:
Caleb
2026-03-01 15:59:27 +08:00
commit a9e97d56cb
1426 changed files with 172367 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
//
// AVLiveStreamModel.h
// AVDemo
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AVLiveStreamModel : NSObject
@property (nonatomic, copy) NSString *vhost;
@property (nonatomic, copy) NSString *app;
@property (nonatomic, copy) NSString *stream;
//如果该字段不为空则为连麦pk房间
@property (nonatomic, copy) NSString *stream_pk;
@property (nonatomic, copy) NSString *url;
@property (nonatomic, assign) NSTimeInterval startTime;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, copy) NSString *preview_image; // 预览图 URL
@property (nonatomic, copy) NSString *play_protocol; // 播放协议rtc, rtmp, flv, hls 等
- (NSString *)displayName;
- (NSString *)durationString;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,29 @@
//
// AVLiveStreamModel.m
// AVDemo
//
#import "AVLiveStreamModel.h"
@implementation AVLiveStreamModel
- (NSString *)displayName {
return self.stream.length > 0 ? self.stream : @"未知流";
}
- (NSString *)durationString {
NSInteger totalSeconds = (NSInteger)self.duration;
NSInteger hours = totalSeconds / 3600;
NSInteger minutes = (totalSeconds % 3600) / 60;
NSInteger seconds = totalSeconds % 60;
if (hours > 0) {
// 1 hh:mm:ss
return [NSString stringWithFormat:@"%02ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds];
} else {
// 1 mm:ss
return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}
}
@end

View File

@@ -0,0 +1,20 @@
//
// SCVODPlayerViewController.h
// SellyCloudSDK_Example
// 直播播放器
// Created by Caleb on 16/9/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
#import "AVLiveStreamModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface SCLiveVideoPlayerViewController : UIViewController
// 移除外部传入的属性,改为内部配置
- (instancetype)initWithLiveStream:(AVLiveStreamModel *)stream;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,798 @@
//
// 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 {
// 使 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 {
//
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;
}
}
#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

View File

@@ -0,0 +1,33 @@
//
// SCPlayerConfigView.h
// SellyCloudSDK_Example
//
// Created by Caleb on 16/12/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface SCPlayerConfig : NSObject
@property (nonatomic, assign) SellyLiveMode protocol;
@property (nonatomic, strong) NSString *streamId;
/// 保存配置到 UserDefaults
- (void)saveToUserDefaults;
/// 从 UserDefaults 加载配置
+ (instancetype)loadFromUserDefaults;
@end
@interface SCPlayerConfigView : UIView
- (void)showInViewController:(UIViewController *)viewController
callback:(void(^)(SCPlayerConfig *config))callback;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,384 @@
//
// SCPlayerConfigView.m
// SellyCloudSDK_Example
//
// Created by Caleb on 16/12/25.
// Copyright © 2025 Caleb. All rights reserved.
//
#import "SCPlayerConfigView.h"
#import <Masonry/Masonry.h>
// key
static NSString * const kSCPlayerConfigProtocol = @"SCPlayerConfigProtocol";
static NSString * const kSCPlayerConfigStreamId = @"SCPlayerConfigStreamId";
@implementation SCPlayerConfig
- (void)saveToUserDefaults {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setInteger:self.protocol forKey:kSCPlayerConfigProtocol];
if (self.streamId) {
[defaults setObject:self.streamId forKey:kSCPlayerConfigStreamId];
}
[defaults synchronize];
}
+ (instancetype)loadFromUserDefaults {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
// RTMP
if ([defaults objectForKey:kSCPlayerConfigProtocol]) {
config.protocol = [defaults integerForKey:kSCPlayerConfigProtocol];
} else {
config.protocol = SellyLiveMode_RTMP;
}
// streamId
NSString *streamId = [defaults objectForKey:kSCPlayerConfigStreamId];
config.streamId = streamId ? streamId : @"test";
return config;
}
@end
@interface SCPlayerConfigView () <UITextFieldDelegate>
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIVisualEffectView *backgroundView;
@property (nonatomic, strong) UISegmentedControl *protocolSegment;
@property (nonatomic, strong) UITextField *streamIdField;
@property (nonatomic, strong) UIButton *playButton;
@property (nonatomic, copy) void(^callback)(SCPlayerConfig *config);
@property (nonatomic, strong) MASConstraint *backgroundViewCenterYConstraint; //
@end
@implementation SCPlayerConfigView
- (instancetype)init {
self = [super init];
if (self) {
[self setupView];
[self loadSavedConfig];
[self registerKeyboardNotifications];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setupView {
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
// -
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(backgroundTapped:)];
tapGesture.cancelsTouchesInView = NO; //
[self addGestureRecognizer:tapGesture];
//
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial];
_backgroundView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
_backgroundView.layer.cornerRadius = 16;
_backgroundView.layer.masksToBounds = YES;
_backgroundView.userInteractionEnabled = YES; //
[self addSubview:_backgroundView];
[_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
//
make.centerX.equalTo(self);
// centerY 便
self.backgroundViewCenterYConstraint = make.centerY.equalTo(self).offset(30); // 30pt
make.left.offset(40);
make.right.offset(-40);
}];
//
_contentView = [[UIView alloc] init];
[_backgroundView.contentView addSubview:_contentView];
[_contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(_backgroundView.contentView).insets(UIEdgeInsetsMake(24, 24, 24, 24));
}];
//
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = @"播放配置";
titleLabel.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
titleLabel.textAlignment = NSTextAlignmentCenter;
[_contentView addSubview:titleLabel];
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_contentView);
make.left.right.equalTo(_contentView);
make.height.offset(30);
}];
//
UILabel *protocolLabel = [[UILabel alloc] init];
protocolLabel.text = @"播放协议";
protocolLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
[_contentView addSubview:protocolLabel];
[protocolLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(titleLabel.mas_bottom).offset(24);
make.left.equalTo(_contentView);
make.height.offset(22);
}];
//
_protocolSegment = [[UISegmentedControl alloc] initWithItems:@[@"RTMP", @"RTC"]];
_protocolSegment.selectedSegmentIndex = 0;
[_contentView addSubview:_protocolSegment];
[_protocolSegment mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(protocolLabel.mas_bottom).offset(8);
make.left.right.equalTo(_contentView);
make.height.offset(36);
}];
// Stream ID
UILabel *streamIdLabel = [[UILabel alloc] init];
streamIdLabel.text = @"Stream ID / URL。请输入 Stream ID 或完整 URL";
streamIdLabel.font = [UIFont systemFontOfSize:14];
streamIdLabel.numberOfLines = 0;
[_contentView addSubview:streamIdLabel];
[streamIdLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_protocolSegment.mas_bottom).offset(24);
make.left.equalTo(_contentView);
make.right.equalTo(_contentView);
make.height.offset(34);
}];
// Stream ID
_streamIdField = [[UITextField alloc] init];
_streamIdField.placeholder = @"请输入 Stream ID 或完整 URL";
_streamIdField.borderStyle = UITextBorderStyleRoundedRect;
_streamIdField.font = [UIFont systemFontOfSize:15];
_streamIdField.clearButtonMode = UITextFieldViewModeWhileEditing;
_streamIdField.returnKeyType = UIReturnKeyDone;
_streamIdField.delegate = self;
//
_streamIdField.autocapitalizationType = UITextAutocapitalizationTypeNone;
//
_streamIdField.autocorrectionType = UITextAutocorrectionTypeNo;
// URL URL
_streamIdField.keyboardType = UIKeyboardTypeURL;
[_contentView addSubview:_streamIdField];
[_streamIdField mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(streamIdLabel.mas_bottom).offset(8);
make.left.right.equalTo(_contentView);
make.height.offset(44);
}];
//
_playButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_playButton setTitle:@"开始播放" forState:UIControlStateNormal];
_playButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightBold];
_playButton.backgroundColor = [UIColor systemBlueColor];
[_playButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_playButton.layer.cornerRadius = 12;
_playButton.layer.masksToBounds = YES;
[_playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[_contentView addSubview:_playButton];
[_playButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_streamIdField.mas_bottom).offset(32);
make.left.right.equalTo(_contentView);
make.height.offset(50);
make.bottom.equalTo(_contentView);
}];
//
}
- (void)playButtonTapped {
NSString *streamId = [_streamIdField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (streamId.length == 0) {
//
[self showAlertWithMessage:@"请输入 Stream ID 或 URL"];
return;
}
SCPlayerConfig *config = [[SCPlayerConfig alloc] init];
config.protocol = _protocolSegment.selectedSegmentIndex == 0 ? SellyLiveMode_RTMP : SellyLiveMode_RTC;
config.streamId = streamId;
//
[config saveToUserDefaults];
if (self.callback) {
self.callback(config);
}
[self dismiss];
}
- (void)showAlertWithMessage:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
// viewController
UIViewController *topVC = [self topViewController];
if (topVC) {
[topVC presentViewController:alert animated:YES completion:nil];
}
}
- (UIViewController *)topViewController {
UIViewController *topVC = nil;
UIWindow *keyWindow = nil;
// iOS 13+ keyWindow
if (@available(iOS 13.0, *)) {
NSSet<UIScene *> *scenes = [UIApplication sharedApplication].connectedScenes;
for (UIScene *scene in scenes) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow) {
keyWindow = window;
break;
}
}
}
if (keyWindow) break;
}
} else {
keyWindow = [UIApplication sharedApplication].keyWindow;
}
topVC = keyWindow.rootViewController;
while (topVC.presentedViewController) {
topVC = topVC.presentedViewController;
}
return topVC;
}
- (void)showInViewController:(UIViewController *)viewController callback:(void (^)(SCPlayerConfig * _Nonnull))callback {
self.callback = callback;
// viewController view window
self.frame = viewController.view.bounds;
[viewController.view addSubview:self];
//
self.alpha = 0;
_backgroundView.transform = CGAffineTransformMakeScale(0.8, 0.8);
[UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.alpha = 1;
self.backgroundView.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)dismiss {
[UIView animateWithDuration:0.2 animations:^{
self.alpha = 0;
self.backgroundView.transform = CGAffineTransformMakeScale(0.8, 0.8);
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
//
- (void)backgroundTapped:(UITapGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:self];
//
if (!CGRectContainsPoint(_backgroundView.frame, location)) {
//
NSLog(@"📱 点击背景关闭配置弹窗");
[self dismiss];
}
}
- (void)loadSavedConfig {
SCPlayerConfig *savedConfig = [SCPlayerConfig loadFromUserDefaults];
//
_protocolSegment.selectedSegmentIndex = (savedConfig.protocol == SellyLiveMode_RTMP) ? 0 : 1;
// streamId
if (savedConfig.streamId && savedConfig.streamId.length > 0) {
_streamIdField.text = savedConfig.streamId;
}
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark - Keyboard Notifications
- (void)registerKeyboardNotifications {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
//
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat keyboardHeight = keyboardFrame.size.height;
// 线
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
//
CGRect textFieldFrame = [_streamIdField convertRect:_streamIdField.bounds toView:self];
CGFloat textFieldBottom = CGRectGetMaxY(textFieldFrame);
//
CGFloat visibleHeight = self.bounds.size.height - keyboardHeight;
// 20pt
CGFloat desiredSpace = 20;
CGFloat offset = 0;
if (textFieldBottom + desiredSpace > visibleHeight) {
//
offset = -(textFieldBottom + desiredSpace - visibleHeight);
}
//
[self.backgroundViewCenterYConstraint setOffset:offset];
//
[UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{
[self layoutIfNeeded];
} completion:nil];
}
- (void)keyboardWillHide:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
// 线
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
//
[self.backgroundViewCenterYConstraint setOffset:30];
//
[UIView animateWithDuration:duration delay:0 options:(curve << 16) animations:^{
[self layoutIfNeeded];
} completion:nil];
}
@end

View File

@@ -0,0 +1,29 @@
//
// SCPlayerDebugView.h
// SellyCloudSDK_Example
//
// Created by Caleb on 07/1/26.
// Copyright © 2026 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface SCPlayerDebugView : UIView
/// 添加一条调试信息(自动添加时间戳)
- (void)appendLog:(NSString *)message;
/// 添加一条调试信息(带自定义标签,如 ⚠️ 🔴 等)
- (void)appendLog:(NSString *)message withPrefix:(nullable NSString *)prefix;
/// 清空所有日志
- (void)clearLogs;
/// 展开状态(默认为 YES
@property (nonatomic, assign) BOOL isExpanded;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,260 @@
//
// SCPlayerDebugView.m
// SellyCloudSDK_Example
//
// Created by Caleb on 07/1/26.
// Copyright © 2026 Caleb. All rights reserved.
//
#import "SCPlayerDebugView.h"
#import <Masonry/Masonry.h>
@interface SCPlayerDebugView ()
@property (nonatomic, strong) UIVisualEffectView *blurView;
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *logLabel;
@property (nonatomic, strong) UIButton *toggleButton;
@property (nonatomic, strong) UIButton *clearButton;
@property (nonatomic, strong) MASConstraint *contentViewHeightConstraint;
@property (nonatomic, strong) NSMutableArray<NSString *> *logs;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@end
@implementation SCPlayerDebugView
- (instancetype)init {
self = [super init];
if (self) {
_isExpanded = YES; //
_logs = [NSMutableArray array];
//
_dateFormatter = [[NSDateFormatter alloc] init];
_dateFormatter.dateFormat = @"HH:mm:ss.SSS";
[self setupView];
}
return self;
}
- (void)setupView {
//
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemUltraThinMaterialDark];
_blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
_blurView.layer.cornerRadius = 12;
_blurView.layer.masksToBounds = YES;
[self addSubview:_blurView];
[_blurView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
//
self.layer.cornerRadius = 12;
self.layer.borderWidth = 0.5;
self.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3].CGColor;
//
_headerView = [[UIView alloc] init];
[_blurView.contentView addSubview:_headerView];
[_headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.right.equalTo(_blurView.contentView);
make.height.offset(44);
}];
//
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleButtonTapped)];
[_headerView addGestureRecognizer:tap];
//
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"🐛 调试日志";
_titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
_titleLabel.textColor = [UIColor whiteColor];
[_headerView addSubview:_titleLabel];
[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(_headerView);
make.left.offset(12);
}];
//
_clearButton = [UIButton buttonWithType:UIButtonTypeSystem];
[_clearButton setImage:[UIImage systemImageNamed:@"trash.circle.fill"] forState:UIControlStateNormal];
_clearButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
[_clearButton addTarget:self action:@selector(clearButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[_headerView addSubview:_clearButton];
[_clearButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(_headerView);
make.right.offset(-44);
make.width.height.offset(24);
}];
// /
_toggleButton = [UIButton buttonWithType:UIButtonTypeSystem];
//
[_toggleButton setImage:[UIImage systemImageNamed:@"chevron.up.circle.fill"] forState:UIControlStateNormal];
_toggleButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
[_toggleButton addTarget:self action:@selector(toggleButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[_headerView addSubview:_toggleButton];
[_toggleButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(_headerView);
make.right.offset(-12);
make.width.height.offset(24);
}];
// 线
UIView *separatorLine = [[UIView alloc] init];
separatorLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2];
[_blurView.contentView addSubview:separatorLine];
[separatorLine mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_headerView.mas_bottom);
make.left.offset(12);
make.right.offset(-12);
make.height.offset(0.5);
}];
//
_contentView = [[UIView alloc] init];
_contentView.clipsToBounds = YES;
[_blurView.contentView addSubview:_contentView];
[_contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(separatorLine.mas_bottom);
make.left.right.bottom.equalTo(_blurView.contentView);
//
self.contentViewHeightConstraint = make.height.offset(200); //
}];
// ScrollView
_scrollView = [[UIScrollView alloc] init];
_scrollView.showsVerticalScrollIndicator = YES;
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.alwaysBounceVertical = YES;
[_contentView addSubview:_scrollView];
[_scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(_contentView).insets(UIEdgeInsetsMake(8, 12, 8, 12));
}];
//
_logLabel = [[UILabel alloc] init];
_logLabel.font = [UIFont monospacedSystemFontOfSize:10 weight:UIFontWeightRegular];
_logLabel.textColor = [UIColor whiteColor];
_logLabel.numberOfLines = 0; //
_logLabel.lineBreakMode = NSLineBreakByCharWrapping;
_logLabel.text = @""; //
[_scrollView addSubview:_logLabel];
[_logLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(_scrollView);
make.width.equalTo(_scrollView); //
}];
}
- (void)toggleButtonTapped {
//
[self setIsExpanded:!self.isExpanded];
}
- (void)clearButtonTapped {
[self clearLogs];
//
UIImpactFeedbackGenerator *feedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[feedback impactOccurred];
}
#pragma mark - Public Methods
- (void)appendLog:(NSString *)message {
[self appendLog:message withPrefix:nil];
}
- (void)appendLog:(NSString *)message withPrefix:(nullable NSString *)prefix {
if (!message || message.length == 0) {
return;
}
//
NSString *timestamp = [self.dateFormatter stringFromDate:[NSDate date]];
//
NSString *logEntry;
// if (prefix && prefix.length > 0) {
// logEntry = [NSString stringWithFormat:@"[%@] %@ %@", timestamp, prefix, message];
// } else {
logEntry = [NSString stringWithFormat:@"[%@] %@", timestamp, message];
// }
//
[self.logs addObject:logEntry];
// 200
if (self.logs.count > 200) {
[self.logs removeObjectAtIndex:0];
}
// UI
[self updateLogDisplay];
}
- (void)clearLogs {
[self.logs removeAllObjects];
[self updateLogDisplay];
}
#pragma mark - Private Methods
- (void)updateLogDisplay {
//
NSString *allLogs = [self.logs componentsJoinedByString:@"\n"];
self.logLabel.text = allLogs;
//
[self.logLabel mas_updateConstraints:^(MASConstraintMaker *make) {
//
}];
[self.logLabel layoutIfNeeded];
//
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat bottomOffset = MAX(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height);
[self.scrollView setContentOffset:CGPointMake(0, bottomOffset) animated:YES];
});
}
- (void)setIsExpanded:(BOOL)isExpanded {
if (_isExpanded == isExpanded) {
return; //
}
_isExpanded = isExpanded;
// UI
CGFloat expandedHeight = 200; //
CGFloat collapsedHeight = 0; //
[UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:UIViewAnimationOptionCurveEaseInOut animations:^{
if (self.isExpanded) {
//
[self.toggleButton setImage:[UIImage systemImageNamed:@"chevron.up.circle.fill"] forState:UIControlStateNormal];
[self.contentViewHeightConstraint setOffset:expandedHeight];
} else {
//
[self.toggleButton setImage:[UIImage systemImageNamed:@"chevron.down.circle.fill"] forState:UIControlStateNormal];
[self.contentViewHeightConstraint setOffset:collapsedHeight];
}
// / alpha
self.contentView.alpha = self.isExpanded ? 1.0 : 0.0;
//
[self.superview layoutIfNeeded];
} completion:nil];
//
UIImpactFeedbackGenerator *feedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
[feedback impactOccurred];
}
@end

View File

@@ -0,0 +1,20 @@
//
// SCVodVideoPlayerViewController.h
// SellyCloudSDK_Example
// 点播播放器
// Created by Caleb on 1/7/26.
// Copyright © 2026 Caleb. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <SellyCloudSDK/SellyCloudManager.h>
#import "AVLiveStreamModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface SCVodVideoPlayerViewController : UIViewController
// 移除外部传入的属性,改为内部配置
- (instancetype)initWithLiveStream:(AVLiveStreamModel *)stream;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,626 @@
//
// SCVodVideoPlayerViewController.m
// SellyCloudSDK_Example
//
// Created by Caleb on 1/7/26.
// Copyright © 2026 Caleb. All rights reserved.
//
#import "SCVodVideoPlayerViewController.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 "SCPlayerDebugView.h"
@interface SCVodVideoPlayerViewController ()<SellyVodPlayerDelegate>
@property (nonatomic, strong) SellyVodVideoPlayer *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;
//
@property (nonatomic, strong) UIView *progressContainerView;
@property (nonatomic, strong) UISlider *progressSlider;
@property (nonatomic, strong) UILabel *currentTimeLabel;
@property (nonatomic, strong) UILabel *totalTimeLabel;
@property (nonatomic, assign) BOOL isDraggingProgress; //
// model
@property (nonatomic, strong) SCLiveItemModel *playPauseModel;
@property (nonatomic, strong) SCLiveItemModel *muteModel;
@property (nonatomic, strong) AVLiveStreamModel *streamModel;
@end
@implementation SCVodVideoPlayerViewController
- (instancetype)initWithLiveStream:(AVLiveStreamModel *)stream {
self = [super init];
self.streamModel = stream;
[self convertStreamModelToPlayerConfig:stream];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.blackColor;
//
self.navigationController.navigationBarHidden = YES;
//
[self.view addSubview:self.playerContainerView];
[self.playerContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
//
[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);
}];
// containerView
[self setupProgressView];
//
[UIApplication sharedApplication].idleTimerDisabled = YES;
[self startPlayWithConfig:self.currentConfig];
}
- (void)convertStreamModelToPlayerConfig:(AVLiveStreamModel *)stream {
if (!stream) {
NSLog(@"❌ stream model is nil");
return;
}
// SCPlayerConfig
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;
}
- (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:@"✅"];
[_debugView appendLog:@"VOD 点播播放器" withPrefix:@"🎬"];
}
- (void)setupProgressView {
//
_progressContainerView = [[UIView alloc] init];
_progressContainerView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
[self.view addSubview:_progressContainerView];
[_progressContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.offset(0);
make.bottom.equalTo(self.containerView.mas_top).offset(-10);
make.height.offset(50);
}];
//
_currentTimeLabel = [[UILabel alloc] init];
_currentTimeLabel.textColor = [UIColor whiteColor];
_currentTimeLabel.font = [UIFont systemFontOfSize:12];
_currentTimeLabel.text = @"00:00";
[_progressContainerView addSubview:_currentTimeLabel];
[_currentTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.offset(16);
make.centerY.offset(0);
make.width.offset(45);
}];
//
_totalTimeLabel = [[UILabel alloc] init];
_totalTimeLabel.textColor = [UIColor whiteColor];
_totalTimeLabel.font = [UIFont systemFontOfSize:12];
_totalTimeLabel.text = @"00:00";
_totalTimeLabel.textAlignment = NSTextAlignmentRight;
[_progressContainerView addSubview:_totalTimeLabel];
[_totalTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.offset(-16);
make.centerY.offset(0);
make.width.offset(45);
}];
//
_progressSlider = [[UISlider alloc] init];
_progressSlider.minimumValue = 0;
_progressSlider.maximumValue = 1;
_progressSlider.value = 0;
_progressSlider.minimumTrackTintColor = [UIColor systemBlueColor];
_progressSlider.maximumTrackTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3];
//
[_progressSlider addTarget:self action:@selector(progressSliderTouchDown:) forControlEvents:UIControlEventTouchDown];
[_progressSlider addTarget:self action:@selector(progressSliderValueChanged:) forControlEvents:UIControlEventValueChanged];
[_progressSlider addTarget:self action:@selector(progressSliderTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[_progressSlider addTarget:self action:@selector(progressSliderTouchUpOutside:) forControlEvents:UIControlEventTouchUpOutside];
[_progressContainerView addSubview:_progressSlider];
[_progressSlider mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.currentTimeLabel.mas_right).offset(8);
make.right.equalTo(self.totalTimeLabel.mas_left).offset(-8);
make.centerY.offset(0);
}];
}
- (void)closeButtonTapped {
//
if (self.player) {
[self.player stop];
}
//
self.navigationController.navigationBarHidden = NO;
//
[self.navigationController popViewControllerAnimated:YES];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.view bringSubviewToFront:self.progressContainerView];
[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;
SellyVodVideoPlayer *player = [[SellyVodVideoPlayer alloc] init];
player.delegate = self;
player.scaleMode = SellyPlayerScalingModeAspectFit;
player.shouldAutoplay = YES;
[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:@"自动播放: YES" 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.debugView appendLog:[NSString stringWithFormat:@"开始播放 URL: %@", config.streamId] withPrefix:@"▶️"];
[self.player startPlayUrl:config.streamId];
} else {
[self.debugView appendLog:@"无效的播放地址" 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 = ^{
weakSelf.player.playbackVolume = 1 - weakSelf.player.playbackVolume;
NSLog(@"📢 点击静音按钮 - 新音量: %.2f", weakSelf.player.playbackVolume);
[weakSelf updateMuteButtonState];
};
self.muteModel = model;
[items addObject:model];
}
{
SCLiveItemModel *model = SCLiveItemModel.new;
model.type = SCLiveItemTypeScreenshot;
model.title = @"截图";
model.clickCallback = ^{
UIImage *screenshot = weakSelf.player.getCurrentImage;
if (screenshot) {
[weakSelf.debugView appendLog:@"截图成功,正在保存..." withPrefix:@"📸"];
} else {
[weakSelf.debugView appendLog:@"截图失败:无图像" withPrefix:@"⚠️"];
}
[weakSelf saveCurrentFrameToPhotoAlbum:screenshot];
};
[items addObject:model];
}
{
SCLiveItemModel *model = SCLiveItemModel.new;
model.type = SCLiveItemTypeSeekForward;
model.title = @"快进10秒";
model.clickCallback = ^{
// 10
NSTimeInterval currentTime = weakSelf.player.currentPlaybackTime;
NSTimeInterval duration = weakSelf.player.duration;
NSTimeInterval newTime = currentTime + 10.0;
//
if (duration > 0 && newTime > duration) {
newTime = duration;
}
weakSelf.player.currentPlaybackTime = newTime;
NSLog(@"⏩ 快进 - 从 %.2f 秒到 %.2f 秒", currentTime, newTime);
[weakSelf.debugView appendLog:[NSString stringWithFormat:@"快进: %.2fs → %.2fs (总时长: %.2fs)",
currentTime, newTime, duration] withPrefix:@"⏩"];
};
[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;
}
#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.debugView appendLog:@"恢复播放" withPrefix:@"▶️"];
[self.player resume];
}
- (void)pause {
[self.debugView appendLog:@"暂停播放" withPrefix:@"⏸️"];
[self.player pause];
}
#pragma mark - Progress Control
- (NSString *)formatTime:(NSTimeInterval)timeInterval {
NSInteger totalSeconds = (NSInteger)timeInterval;
NSInteger hours = totalSeconds / 3600;
NSInteger minutes = (totalSeconds % 3600) / 60;
NSInteger seconds = totalSeconds % 60;
if (hours > 0) {
return [NSString stringWithFormat:@"%02ld:%02ld:%02ld", (long)hours, (long)minutes, (long)seconds];
} else {
return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}
}
- (void)progressSliderTouchDown:(UISlider *)slider {
self.isDraggingProgress = YES;
[self.debugView appendLog:@"开始拖动进度条" withPrefix:@"👆"];
}
- (void)progressSliderValueChanged:(UISlider *)slider {
if (self.isDraggingProgress) {
//
self.currentTimeLabel.text = [self formatTime:slider.value];
}
}
- (void)progressSliderTouchUpInside:(UISlider *)slider {
[self seekToTime:slider.value];
}
- (void)progressSliderTouchUpOutside:(UISlider *)slider {
[self seekToTime:slider.value];
}
- (void)seekToTime:(NSTimeInterval)time {
self.isDraggingProgress = NO;
if (self.player) {
self.player.currentPlaybackTime = time;
[self.debugView appendLog:[NSString stringWithFormat:@"跳转到: %@", [self formatTime:time]] withPrefix:@"⏭️"];
}
}
#pragma mark - SellyPlayerManagerDelegate
- (void)player:(SellyVodVideoPlayer *)player prepareToPlayChanged:(BOOL)prepare {
NSLog(@"Player prepare changed: %d", prepare);
[self.debugView appendLog:[NSString stringWithFormat:@"prepareToPlayChanged: %@", prepare ? @"YES" : @"NO"] withPrefix:@"🔧"];
}
- (void)player:(SellyVodVideoPlayer *)player playbackDidFinished:(NSDictionary *)resultInfo {
NSLog(@"Playback finished: %@", resultInfo);
[self.debugView appendLog:@"播放已结束" withPrefix:@"⏹️"];
}
- (void)player:(SellyVodVideoPlayer *)player playbackStateChanged:(SellyPlayerState)state {
NSString *stateString = [self stringFromPlayerState:state];
NSLog(@"🎬 播放状态变更: %@ (rawValue: %ld)", stateString, (long)state);
//
[self.debugView appendLog:[NSString stringWithFormat:@"状态变更: %@", stateString] withPrefix:@"🎬"];
// /
[self updatePlayPauseButtonState];
//
// playbackProgressChanged
}
- (void)player:(SellyVodVideoPlayer *)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:(SellyVodVideoPlayer *)player firstRemoteVideoFrame:(NSInteger)elapse {
NSLog(@"###视频首帧加载耗时 == %ldms",elapse);
[self.debugView appendLog:[NSString stringWithFormat:@"视频首帧: %ldms", elapse] withPrefix:@"📹"];
self.progressSlider.maximumValue = self.player.duration;
self.totalTimeLabel.text = [self formatTime:self.player.duration];
}
- (void)player:(SellyVodVideoPlayer *)player firstRemoteAudioFrame:(NSInteger)elapse {
NSLog(@"###语音首帧加载耗时 == %ldms",elapse);
[self.debugView appendLog:[NSString stringWithFormat:@"音频首帧: %ldms", elapse] withPrefix:@"🔊"];
}
- (void)player:(SellyVodVideoPlayer *)player
playbackProgressChanged:(NSTimeInterval)currentTime {
//
// currentTime
if (self.isDraggingProgress) {
//
return;
}
//
NSTimeInterval duration = self.player.duration;
//
if (duration > 0) {
self.progressSlider.value = currentTime;
self.currentTimeLabel.text = [self formatTime:currentTime];
}
//
// NSLog(@"⏱️ 播放进度: %.2f / %.2f 秒 (%.1f%%)", currentTime, duration, (currentTime/duration)*100);
}
- (void)player:(SellyVodVideoPlayer *)player
bufferProgressChanged:(NSTimeInterval)playableDuration {
//
// 使
//
NSTimeInterval duration = self.player.duration;
if (duration > 0) {
CGFloat bufferProgress = playableDuration / duration;
//
// self.bufferProgressView.progress = bufferProgress;
// NSLog(@"📶 缓冲进度: %.2f / %.2f 秒 (%.1f%%)", playableDuration, duration, bufferProgress * 100);
}
}
#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);
[self.debugView appendLog:[NSString stringWithFormat:@"保存失败: %@", error.localizedDescription] withPrefix:@"🔴"];
} else {
NSLog(@"已保存当前视频帧至相册");
[self.debugView appendLog:@"已保存至相册" withPrefix:@"✅"];
}
}
#pragma mark - Lazy Loading
- (UIView *)playerContainerView {
if (!_playerContainerView) {
_playerContainerView = [[UIView alloc] init];
_playerContainerView.backgroundColor = [UIColor blackColor];
}
return _playerContainerView;
}
- (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;
}
@end