627 lines
22 KiB
Objective-C
627 lines
22 KiB
Objective-C
//
|
||
// 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 {
|
||
// 默认使用 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
|