// // JXSuperPlayer.m // AICity // // Created by TogetherWatch on 2025-10-20. // #import "JXSuperPlayer.h" #import @interface JXSuperPlayer () @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