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,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