| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- //
- // JXSuperPlayer.m
- // AICity
- //
- // Created by TogetherWatch on 2025-10-20.
- //
- #import "JXSuperPlayer.h"
- #import <TXLiteAVSDK_Player/TXLiteAVSDK.h>
- @interface JXSuperPlayer () <TXVodPlayListener>
- @property (nonatomic, strong) TXVodPlayer *vodPlayer;
- @property (nonatomic, strong) UIView *containerView;
- @property (nonatomic, strong) UIView *playerView;
- @property (nonatomic, assign) JXSuperPlayerState state;
- @property (nonatomic, strong) NSTimer *progressTimer;
- // 重试相关
- @property (nonatomic, assign) NSInteger retryCount;
- @property (nonatomic, assign) NSInteger maxRetryCount;
- @property (nonatomic, strong) NSDictionary *lastPlayParams;
- @property (nonatomic, assign) BOOL isRetrying;
- // 私有方法声明
- - (void)updateState:(JXSuperPlayerState)state;
- - (void)notifyError:(NSError *)error;
- - (void)startProgressTimer;
- - (void)stopProgressTimer;
- - (void)updateProgress;
- - (void)handlePlayError:(NSError *)error;
- - (void)internalPlayWithParams:(NSDictionary *)params;
- @end
- @implementation JXSuperPlayer
- #pragma mark - 初始化
- - (instancetype)initWithContainerView:(UIView *)containerView {
- self = [super init];
- if (self) {
- _containerView = containerView;
- _state = JXSuperPlayerStateIdle;
- _maxRetryCount = 2; // 最多重试2次
- _retryCount = 0;
- _isRetrying = NO;
- [self setupPlayer];
- }
- return self;
- }
- - (void)setupPlayer {
- // 创建播放器实例
- self.vodPlayer = [[TXVodPlayer alloc] init];
- // 设置播放器代理
- self.vodPlayer.vodDelegate = self;
-
- // 创建播放器视图
- self.playerView = [[UIView alloc] initWithFrame:self.containerView.bounds];
- self.playerView.backgroundColor = [UIColor blackColor];
- [self.containerView addSubview:self.playerView];
-
- // 设置渲染视图
- [self.vodPlayer setupVideoWidget:self.playerView insertIndex:0];
-
- // 配置播放器
- [self configurePlayer];
-
- NSLog(@"[JXSuperPlayer] 播放器初始化完成");
- }
- - (void)configurePlayer {
- // 设置腾讯云SDK日志级别,显示所有级别日志
- [TXLiveBase setLogLevel:LOGLEVEL_NULL];
- // 注意:TXVodPlayer没有setLogLevel方法,日志级别通过TXLiveBase全局设置
- // 注意:TXVodPlayer没有setConfig:forKey:方法,TPPlayer日志通过环境变量控制
- // 配置点播播放器
- TXVodPlayConfig *config = [[TXVodPlayConfig alloc] init];
- // 缓存配置
- config.maxCacheItems = 5;
- config.maxBufferSize = 50; // MB
- // 首帧优化
- config.preferredResolution = 720 * 1280;
- // 进度更新间隔(毫秒)
- config.progressInterval = 500;
- // 设置网络超时和缓冲配置
- config.timeout = 60.0f; // 60秒超时(更长的超时时间)
- config.maxBufferSize = 100; // 100MB缓冲区
- config.maxPreloadSize = 50; // 50MB预加载
- config.maxCacheItems = 10; // 最大缓存项目数
- config.smoothSwitchBitrate = YES; // 码率切换平滑过渡
- [self.vodPlayer setConfig:config];
- // 设置渲染模式(铺满)
- [self.vodPlayer setRenderMode:RENDER_MODE_FILL_SCREEN];
-
- // 注意:TXVodPlayer的控制栏UI是内置的,可以通过手势隐藏
- // 在setupVideoWidget时已自动集成
- }
- #pragma mark - 播放控制
- - (void)playWithAppId:(NSString *)appId
- fileId:(NSString *)fileId
- psign:(NSString *)psign {
- // 重置重试计数(如果不是重试)
- if (!self.isRetrying) {
- self.retryCount = 0;
- self.isRetrying = NO;
- }
- // 保存播放参数(用于重试)
- self.lastPlayParams = @{
- @"type": @"fileid",
- @"appId": appId,
- @"fileId": fileId,
- @"psign": psign
- };
- [self internalPlayWithParams:self.lastPlayParams];
- }
- - (void)internalPlayWithParams:(NSDictionary *)params {
- NSString *type = params[@"type"];
- if ([type isEqualToString:@"fileid"]) {
- NSString *appId = params[@"appId"];
- NSString *fileId = params[@"fileId"];
- NSString *psign = params[@"psign"];
- NSLog(@"[JXSuperPlayer] FileID播放: appId=%@, fileId=%@, retryCount=%ld", appId, fileId, (long)self.retryCount);
- // 设置状态
- [self updateState:JXSuperPlayerStatePreparing];
- // 创建FileID播放参数
- TXPlayerAuthParams *txParams = [[TXPlayerAuthParams alloc] init];
- txParams.appId = [appId intValue];
- txParams.fileId = fileId;
- txParams.sign = psign;
- // 开始播放
- int result = [self.vodPlayer startVodPlayWithParams:txParams];
- if (result != 0) {
- NSLog(@"[JXSuperPlayer] FileID播放启动失败: %d", result);
- NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
- code:result
- userInfo:@{NSLocalizedDescriptionKey: @"FileID播放启动失败"}];
- [self handlePlayError:error];
- } else {
- [self startProgressTimer];
- }
- } else if ([type isEqualToString:@"url"]) {
- NSString *url = params[@"url"];
- NSLog(@"[JXSuperPlayer] URL播放: %@, retryCount=%ld", url, (long)self.retryCount);
- // 设置状态
- [self updateState:JXSuperPlayerStatePreparing];
- // 开始播放
- int result = [self.vodPlayer startVodPlay:url];
- if (result != 0) {
- NSLog(@"[JXSuperPlayer] URL播放启动失败: %d", result);
- NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
- code:result
- userInfo:@{NSLocalizedDescriptionKey: @"URL播放启动失败"}];
- [self handlePlayError:error];
- } else {
- [self startProgressTimer];
- }
- }
- }
- - (void)playWithURL:(NSString *)url {
- // 重置重试计数(如果不是重试)
- if (!self.isRetrying) {
- self.retryCount = 0;
- self.isRetrying = NO;
- }
- // 保存播放参数(用于重试)
- self.lastPlayParams = @{
- @"type": @"url",
- @"url": url
- };
- [self internalPlayWithParams:self.lastPlayParams];
- }
- - (void)play {
- [self.vodPlayer resume];
- [self updateState:JXSuperPlayerStatePlaying];
- [self startProgressTimer];
- }
- - (void)pause {
- NSLog(@"[JXSuperPlayer] ⚠️ pause 被调用!");
- NSLog(@"[JXSuperPlayer] 调用栈前5层:");
- NSArray *stack = [NSThread callStackSymbols];
- for (int i = 0; i < MIN(5, stack.count); i++) {
- NSLog(@" %@", stack[i]);
- }
-
- [self.vodPlayer pause];
- [self updateState:JXSuperPlayerStatePaused];
- [self stopProgressTimer];
- }
- - (void)stop {
- [self.vodPlayer stopPlay];
- [self updateState:JXSuperPlayerStateIdle];
- [self stopProgressTimer];
- }
- - (void)resume {
- [self play];
- }
- - (void)seekToTime:(NSTimeInterval)time {
- [self.vodPlayer seek:time];
- }
- #pragma mark - 播放信息
- - (NSTimeInterval)currentTime {
- return [self.vodPlayer currentPlaybackTime];
- }
- - (NSTimeInterval)duration {
- return [self.vodPlayer duration];
- }
- - (BOOL)isPlaying {
- return self.state == JXSuperPlayerStatePlaying;
- }
- #pragma mark - TXVodPlayListener 代理方法
- - (void)onPlayEvent:(TXVodPlayer *)player event:(int)EvtID withParam:(NSDictionary *)param {
- switch (EvtID) {
- case PLAY_EVT_VOD_PLAY_PREPARED:
- NSLog(@"[JXSuperPlayer] 播放器准备完成");
- break;
-
- case PLAY_EVT_PLAY_BEGIN:
- NSLog(@"[JXSuperPlayer] 开始播放");
- [self updateState:JXSuperPlayerStatePlaying];
- break;
-
- case PLAY_EVT_PLAY_PROGRESS: {
- // 播放进度(由progressTimer处理,这里不处理)
-
- float progress = [param[@"EVT_PLAY_PROGRESS"] floatValue] / [param[@"EVT_PLAY_DURATION"] floatValue] ;
- [[NSNotificationCenter defaultCenter] postNotificationName:@"videoProgressChanged" object:nil userInfo:@{@"progress":@(progress),@"fileId":self.lastPlayParams[@"fileId"]}];
- break;
- }
-
- case PLAY_EVT_PLAY_END:
- NSLog(@"[JXSuperPlayer] 播放完成");
- [self updateState:JXSuperPlayerStateCompleted];
- [self stopProgressTimer];
- break;
-
- case PLAY_EVT_PLAY_LOADING:
- NSLog(@"[JXSuperPlayer] 加载中...");
- break;
-
- case PLAY_EVT_RCV_FIRST_I_FRAME:
- NSLog(@"[JXSuperPlayer] 首帧渲染");
- break;
- case PLAY_ERR_NET_DISCONNECT:
- case PLAY_ERR_HLS_KEY:
- case PLAY_ERR_GET_PLAYINFO_FAIL: {
- NSLog(@"[JXSuperPlayer] 播放错误: %d", EvtID);
- NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
- code:EvtID
- userInfo:@{NSLocalizedDescriptionKey: @"播放器内部错误"}];
- [self handlePlayError:error];
- break;
- }
- default:
- break;
- }
- }
- - (void)onNetStatus:(TXVodPlayer *)player withParam:(NSDictionary *)param {
- // 网络状态监控回调
- // 这里接收腾讯云SDK的网络状态参数
-
- // 获取网络相关参数
- NSNumber *videoDecoder = param[@"VIDEO_DECODER"];
- NSNumber *videoBitrate = param[@"VIDEO_BITRATE"];
- NSNumber *audioBitrate = param[@"AUDIO_BITRATE"];
- NSNumber *bufferBytes = param[@"CACHE_SIZE"];
- NSNumber *totalBytes = param[@"TOTAL_BYTES"];
- NSNumber *fps = param[@"VIDEO_FPS"];
- // 只在网络参数有意义变化时打印日志(避免过多日志)
- // 当有实际的码率、缓冲或FPS数据时打印
- static NSInteger lastBitrate = 0;
- NSInteger currentBitrate = [videoBitrate integerValue];
- if (currentBitrate > 0 && currentBitrate != lastBitrate) {
- lastBitrate = currentBitrate;
- NSLog(@"[JXSuperPlayer] 网络状态 - 视频码率:%@ Kbps, 音频码率:%@ Kbps, FPS:%@",
- videoBitrate, audioBitrate, fps);
- }
- }
- #pragma mark - 进度定时器
- - (void)startProgressTimer {
- [self stopProgressTimer];
-
- self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
- target:self
- selector:@selector(updateProgress)
- userInfo:nil
- repeats:YES];
- }
- - (void)stopProgressTimer {
- if (self.progressTimer) {
- [self.progressTimer invalidate];
- self.progressTimer = nil;
- }
- }
- - (void)updateProgress {
- NSTimeInterval currentTime = [self currentTime];
- NSTimeInterval duration = [self duration];
-
- if (duration > 0 && [self.delegate respondsToSelector:@selector(superPlayerDidUpdateProgress:duration:)]) {
- [self.delegate superPlayerDidUpdateProgress:currentTime duration:duration];
- }
- }
- #pragma mark - 状态管理
- - (void)updateState:(JXSuperPlayerState)state {
- if (_state != state) {
- _state = state;
-
- NSLog(@"[JXSuperPlayer] 状态变更: %ld", (long)state);
-
- if ([self.delegate respondsToSelector:@selector(superPlayerDidChangeState:)]) {
- [self.delegate superPlayerDidChangeState:state];
- }
- }
- }
- - (void)notifyError:(NSError *)error {
- [self updateState:JXSuperPlayerStateError];
-
- if ([self.delegate respondsToSelector:@selector(superPlayerDidFailWithError:)]) {
- [self.delegate superPlayerDidFailWithError:error];
- }
- }
- #pragma mark - 资源释放
- - (void)releasePlayer {
- NSLog(@"[JXSuperPlayer] 释放播放器资源");
-
- // 停止进度定时器
- [self stopProgressTimer];
-
- // 完全停止播放并清理
- if (self.vodPlayer) {
- // 先停止播放
- [self.vodPlayer stopPlay];
-
- // 移除视图
- [self.vodPlayer removeVideoWidget];
-
- // 重置播放器配置,确保下次创建时是全新状态
- self.vodPlayer.vodDelegate = nil;
-
- // 释放播放器实例
- self.vodPlayer = nil;
- }
-
- // 清理视图
- if (self.playerView) {
- [self.playerView removeFromSuperview];
- self.playerView = nil;
- }
-
- // 重置状态
- [self updateState:JXSuperPlayerStateIdle];
-
- NSLog(@"[JXSuperPlayer] 播放器资源已完全释放");
- }
- - (void)dealloc {
- [self releasePlayer];
- NSLog(@"[JXSuperPlayer] dealloc");
- }
- #pragma mark - 工具方法
- + (NSString *)extractAppIdFromPsign:(NSString *)psign {
- if (!psign || psign.length == 0) {
- return nil;
- }
-
- // JWT格式: header.payload.signature
- NSArray *components = [psign componentsSeparatedByString:@"."];
- if (components.count < 2) {
- NSLog(@"[JXSuperPlayer] psign格式错误");
- return nil;
- }
-
- // 解析payload(Base64编码)
- NSString *payloadBase64 = components[1];
-
- // Base64解码
- NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:payloadBase64 options:0];
- if (!decodedData) {
- NSLog(@"[JXSuperPlayer] Base64解码失败");
- return nil;
- }
-
- // JSON解析
- NSError *error = nil;
- NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:decodedData
- options:0
- error:&error];
- if (error || ![payload isKindOfClass:[NSDictionary class]]) {
- NSLog(@"[JXSuperPlayer] JSON解析失败: %@", error);
- return nil;
- }
-
- // 提取appId
- id appIdValue = payload[@"appId"];
- if ([appIdValue isKindOfClass:[NSNumber class]]) {
- return [(NSNumber *)appIdValue stringValue];
- } else if ([appIdValue isKindOfClass:[NSString class]]) {
- return (NSString *)appIdValue;
- }
-
- NSLog(@"[JXSuperPlayer] psign中未找到appId");
- return nil;
- }
- #pragma mark - 错误处理和重试
- - (void)handlePlayError:(NSError *)error {
- // 检查是否是网络超时错误
- BOOL isNetworkTimeout = [error.domain isEqualToString:NSURLErrorDomain] &&
- (error.code == NSURLErrorTimedOut || error.code == NSURLErrorCannotConnectToHost);
- // 如果是网络错误且未达到最大重试次数,则重试
- if (isNetworkTimeout && self.retryCount < self.maxRetryCount) {
- self.retryCount++;
- self.isRetrying = YES;
- NSLog(@"[JXSuperPlayer] 播放失败,准备重试 (%ld/%ld): %@", (long)self.retryCount, (long)self.maxRetryCount, error.localizedDescription);
- // 延迟2秒后重试
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [self internalPlayWithParams:self.lastPlayParams];
- });
- } else {
- // 重试次数用完或非网络错误,直接通知错误
- if (self.retryCount >= self.maxRetryCount) {
- NSLog(@"[JXSuperPlayer] 重试次数用完,仍播放失败");
- }
- [self updateState:JXSuperPlayerStateError];
- [self notifyError:error];
- }
- }
- @end
|