// // 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 #import #import "SCLiveItemContainerView.h" #import #import "SCPlayerDebugView.h" @interface SCVodVideoPlayerViewController () @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 { // 默认使用 RTMP(flv、hls 等也使用 RTMP 模式) config.protocol = SellyLiveMode_RTMP; } NSLog(@"🔄 转换 StreamModel -> PlayerConfig: stream=%@, protocol=%@ (%@)", config.streamId, stream.play_protocol, config.protocol == SellyLiveMode_RTC ? @"RTC" : @"RTMP"); // 保存配置并开始播放 self.currentConfig = config; } - (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