JXSuperPlayer.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. //
  2. // JXSuperPlayer.m
  3. // AICity
  4. //
  5. // Created by TogetherWatch on 2025-10-20.
  6. //
  7. #import "JXSuperPlayer.h"
  8. #import <TXLiteAVSDK_Player/TXLiteAVSDK.h>
  9. @interface JXSuperPlayer () <TXVodPlayListener>
  10. @property (nonatomic, strong) TXVodPlayer *vodPlayer;
  11. @property (nonatomic, strong) UIView *containerView;
  12. @property (nonatomic, strong) UIView *playerView;
  13. @property (nonatomic, assign) JXSuperPlayerState state;
  14. @property (nonatomic, strong) NSTimer *progressTimer;
  15. // 重试相关
  16. @property (nonatomic, assign) NSInteger retryCount;
  17. @property (nonatomic, assign) NSInteger maxRetryCount;
  18. @property (nonatomic, strong) NSDictionary *lastPlayParams;
  19. @property (nonatomic, assign) BOOL isRetrying;
  20. // 私有方法声明
  21. - (void)updateState:(JXSuperPlayerState)state;
  22. - (void)notifyError:(NSError *)error;
  23. - (void)startProgressTimer;
  24. - (void)stopProgressTimer;
  25. - (void)updateProgress;
  26. - (void)handlePlayError:(NSError *)error;
  27. - (void)internalPlayWithParams:(NSDictionary *)params;
  28. @end
  29. @implementation JXSuperPlayer
  30. #pragma mark - 初始化
  31. - (instancetype)initWithContainerView:(UIView *)containerView {
  32. self = [super init];
  33. if (self) {
  34. _containerView = containerView;
  35. _state = JXSuperPlayerStateIdle;
  36. _maxRetryCount = 2; // 最多重试2次
  37. _retryCount = 0;
  38. _isRetrying = NO;
  39. [self setupPlayer];
  40. }
  41. return self;
  42. }
  43. - (void)setupPlayer {
  44. // 创建播放器实例
  45. self.vodPlayer = [[TXVodPlayer alloc] init];
  46. // 设置播放器代理
  47. self.vodPlayer.vodDelegate = self;
  48. // 创建播放器视图
  49. self.playerView = [[UIView alloc] initWithFrame:self.containerView.bounds];
  50. self.playerView.backgroundColor = [UIColor blackColor];
  51. [self.containerView addSubview:self.playerView];
  52. // 设置渲染视图
  53. [self.vodPlayer setupVideoWidget:self.playerView insertIndex:0];
  54. // 配置播放器
  55. [self configurePlayer];
  56. NSLog(@"[JXSuperPlayer] 播放器初始化完成");
  57. }
  58. - (void)configurePlayer {
  59. // 设置腾讯云SDK日志级别,显示所有级别日志
  60. [TXLiveBase setLogLevel:LOGLEVEL_NULL];
  61. // 注意:TXVodPlayer没有setLogLevel方法,日志级别通过TXLiveBase全局设置
  62. // 注意:TXVodPlayer没有setConfig:forKey:方法,TPPlayer日志通过环境变量控制
  63. // 配置点播播放器
  64. TXVodPlayConfig *config = [[TXVodPlayConfig alloc] init];
  65. // 缓存配置
  66. config.maxCacheItems = 5;
  67. config.maxBufferSize = 50; // MB
  68. // 首帧优化
  69. config.preferredResolution = 720 * 1280;
  70. // 进度更新间隔(毫秒)
  71. config.progressInterval = 500;
  72. // 设置网络超时和缓冲配置
  73. config.timeout = 60.0f; // 60秒超时(更长的超时时间)
  74. config.maxBufferSize = 100; // 100MB缓冲区
  75. config.maxPreloadSize = 50; // 50MB预加载
  76. config.maxCacheItems = 10; // 最大缓存项目数
  77. config.smoothSwitchBitrate = YES; // 码率切换平滑过渡
  78. [self.vodPlayer setConfig:config];
  79. // 设置渲染模式(铺满)
  80. [self.vodPlayer setRenderMode:RENDER_MODE_FILL_SCREEN];
  81. // 注意:TXVodPlayer的控制栏UI是内置的,可以通过手势隐藏
  82. // 在setupVideoWidget时已自动集成
  83. }
  84. #pragma mark - 播放控制
  85. - (void)playWithAppId:(NSString *)appId
  86. fileId:(NSString *)fileId
  87. psign:(NSString *)psign {
  88. // 重置重试计数(如果不是重试)
  89. if (!self.isRetrying) {
  90. self.retryCount = 0;
  91. self.isRetrying = NO;
  92. }
  93. // 保存播放参数(用于重试)
  94. self.lastPlayParams = @{
  95. @"type": @"fileid",
  96. @"appId": appId,
  97. @"fileId": fileId,
  98. @"psign": psign
  99. };
  100. [self internalPlayWithParams:self.lastPlayParams];
  101. }
  102. - (void)internalPlayWithParams:(NSDictionary *)params {
  103. NSString *type = params[@"type"];
  104. if ([type isEqualToString:@"fileid"]) {
  105. NSString *appId = params[@"appId"];
  106. NSString *fileId = params[@"fileId"];
  107. NSString *psign = params[@"psign"];
  108. NSLog(@"[JXSuperPlayer] FileID播放: appId=%@, fileId=%@, retryCount=%ld", appId, fileId, (long)self.retryCount);
  109. // 设置状态
  110. [self updateState:JXSuperPlayerStatePreparing];
  111. // 创建FileID播放参数
  112. TXPlayerAuthParams *txParams = [[TXPlayerAuthParams alloc] init];
  113. txParams.appId = [appId intValue];
  114. txParams.fileId = fileId;
  115. txParams.sign = psign;
  116. // 开始播放
  117. int result = [self.vodPlayer startVodPlayWithParams:txParams];
  118. if (result != 0) {
  119. NSLog(@"[JXSuperPlayer] FileID播放启动失败: %d", result);
  120. NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
  121. code:result
  122. userInfo:@{NSLocalizedDescriptionKey: @"FileID播放启动失败"}];
  123. [self handlePlayError:error];
  124. } else {
  125. [self startProgressTimer];
  126. }
  127. } else if ([type isEqualToString:@"url"]) {
  128. NSString *url = params[@"url"];
  129. NSLog(@"[JXSuperPlayer] URL播放: %@, retryCount=%ld", url, (long)self.retryCount);
  130. // 设置状态
  131. [self updateState:JXSuperPlayerStatePreparing];
  132. // 开始播放
  133. int result = [self.vodPlayer startVodPlay:url];
  134. if (result != 0) {
  135. NSLog(@"[JXSuperPlayer] URL播放启动失败: %d", result);
  136. NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
  137. code:result
  138. userInfo:@{NSLocalizedDescriptionKey: @"URL播放启动失败"}];
  139. [self handlePlayError:error];
  140. } else {
  141. [self startProgressTimer];
  142. }
  143. }
  144. }
  145. - (void)playWithURL:(NSString *)url {
  146. // 重置重试计数(如果不是重试)
  147. if (!self.isRetrying) {
  148. self.retryCount = 0;
  149. self.isRetrying = NO;
  150. }
  151. // 保存播放参数(用于重试)
  152. self.lastPlayParams = @{
  153. @"type": @"url",
  154. @"url": url
  155. };
  156. [self internalPlayWithParams:self.lastPlayParams];
  157. }
  158. - (void)play {
  159. [self.vodPlayer resume];
  160. [self updateState:JXSuperPlayerStatePlaying];
  161. [self startProgressTimer];
  162. }
  163. - (void)pause {
  164. NSLog(@"[JXSuperPlayer] ⚠️ pause 被调用!");
  165. NSLog(@"[JXSuperPlayer] 调用栈前5层:");
  166. NSArray *stack = [NSThread callStackSymbols];
  167. for (int i = 0; i < MIN(5, stack.count); i++) {
  168. NSLog(@" %@", stack[i]);
  169. }
  170. [self.vodPlayer pause];
  171. [self updateState:JXSuperPlayerStatePaused];
  172. [self stopProgressTimer];
  173. }
  174. - (void)stop {
  175. [self.vodPlayer stopPlay];
  176. [self updateState:JXSuperPlayerStateIdle];
  177. [self stopProgressTimer];
  178. }
  179. - (void)resume {
  180. [self play];
  181. }
  182. - (void)seekToTime:(NSTimeInterval)time {
  183. [self.vodPlayer seek:time];
  184. }
  185. #pragma mark - 播放信息
  186. - (NSTimeInterval)currentTime {
  187. return [self.vodPlayer currentPlaybackTime];
  188. }
  189. - (NSTimeInterval)duration {
  190. return [self.vodPlayer duration];
  191. }
  192. - (BOOL)isPlaying {
  193. return self.state == JXSuperPlayerStatePlaying;
  194. }
  195. #pragma mark - TXVodPlayListener 代理方法
  196. - (void)onPlayEvent:(TXVodPlayer *)player event:(int)EvtID withParam:(NSDictionary *)param {
  197. switch (EvtID) {
  198. case PLAY_EVT_VOD_PLAY_PREPARED:
  199. NSLog(@"[JXSuperPlayer] 播放器准备完成");
  200. break;
  201. case PLAY_EVT_PLAY_BEGIN:
  202. NSLog(@"[JXSuperPlayer] 开始播放");
  203. [self updateState:JXSuperPlayerStatePlaying];
  204. break;
  205. case PLAY_EVT_PLAY_PROGRESS: {
  206. // 播放进度(由progressTimer处理,这里不处理)
  207. float progress = [param[@"EVT_PLAY_PROGRESS"] floatValue] / [param[@"EVT_PLAY_DURATION"] floatValue] ;
  208. [[NSNotificationCenter defaultCenter] postNotificationName:@"videoProgressChanged" object:nil userInfo:@{@"progress":@(progress),@"fileId":self.lastPlayParams[@"fileId"]}];
  209. break;
  210. }
  211. case PLAY_EVT_PLAY_END:
  212. NSLog(@"[JXSuperPlayer] 播放完成");
  213. [self updateState:JXSuperPlayerStateCompleted];
  214. [self stopProgressTimer];
  215. break;
  216. case PLAY_EVT_PLAY_LOADING:
  217. NSLog(@"[JXSuperPlayer] 加载中...");
  218. break;
  219. case PLAY_EVT_RCV_FIRST_I_FRAME:
  220. NSLog(@"[JXSuperPlayer] 首帧渲染");
  221. break;
  222. case PLAY_ERR_NET_DISCONNECT:
  223. case PLAY_ERR_HLS_KEY:
  224. case PLAY_ERR_GET_PLAYINFO_FAIL: {
  225. NSLog(@"[JXSuperPlayer] 播放错误: %d", EvtID);
  226. NSError *error = [NSError errorWithDomain:@"JXSuperPlayer"
  227. code:EvtID
  228. userInfo:@{NSLocalizedDescriptionKey: @"播放器内部错误"}];
  229. [self handlePlayError:error];
  230. break;
  231. }
  232. default:
  233. break;
  234. }
  235. }
  236. - (void)onNetStatus:(TXVodPlayer *)player withParam:(NSDictionary *)param {
  237. // 网络状态监控回调
  238. // 这里接收腾讯云SDK的网络状态参数
  239. // 获取网络相关参数
  240. NSNumber *videoDecoder = param[@"VIDEO_DECODER"];
  241. NSNumber *videoBitrate = param[@"VIDEO_BITRATE"];
  242. NSNumber *audioBitrate = param[@"AUDIO_BITRATE"];
  243. NSNumber *bufferBytes = param[@"CACHE_SIZE"];
  244. NSNumber *totalBytes = param[@"TOTAL_BYTES"];
  245. NSNumber *fps = param[@"VIDEO_FPS"];
  246. // 只在网络参数有意义变化时打印日志(避免过多日志)
  247. // 当有实际的码率、缓冲或FPS数据时打印
  248. static NSInteger lastBitrate = 0;
  249. NSInteger currentBitrate = [videoBitrate integerValue];
  250. if (currentBitrate > 0 && currentBitrate != lastBitrate) {
  251. lastBitrate = currentBitrate;
  252. NSLog(@"[JXSuperPlayer] 网络状态 - 视频码率:%@ Kbps, 音频码率:%@ Kbps, FPS:%@",
  253. videoBitrate, audioBitrate, fps);
  254. }
  255. }
  256. #pragma mark - 进度定时器
  257. - (void)startProgressTimer {
  258. [self stopProgressTimer];
  259. self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
  260. target:self
  261. selector:@selector(updateProgress)
  262. userInfo:nil
  263. repeats:YES];
  264. }
  265. - (void)stopProgressTimer {
  266. if (self.progressTimer) {
  267. [self.progressTimer invalidate];
  268. self.progressTimer = nil;
  269. }
  270. }
  271. - (void)updateProgress {
  272. NSTimeInterval currentTime = [self currentTime];
  273. NSTimeInterval duration = [self duration];
  274. if (duration > 0 && [self.delegate respondsToSelector:@selector(superPlayerDidUpdateProgress:duration:)]) {
  275. [self.delegate superPlayerDidUpdateProgress:currentTime duration:duration];
  276. }
  277. }
  278. #pragma mark - 状态管理
  279. - (void)updateState:(JXSuperPlayerState)state {
  280. if (_state != state) {
  281. _state = state;
  282. NSLog(@"[JXSuperPlayer] 状态变更: %ld", (long)state);
  283. if ([self.delegate respondsToSelector:@selector(superPlayerDidChangeState:)]) {
  284. [self.delegate superPlayerDidChangeState:state];
  285. }
  286. }
  287. }
  288. - (void)notifyError:(NSError *)error {
  289. [self updateState:JXSuperPlayerStateError];
  290. if ([self.delegate respondsToSelector:@selector(superPlayerDidFailWithError:)]) {
  291. [self.delegate superPlayerDidFailWithError:error];
  292. }
  293. }
  294. #pragma mark - 资源释放
  295. - (void)releasePlayer {
  296. NSLog(@"[JXSuperPlayer] 释放播放器资源");
  297. // 停止进度定时器
  298. [self stopProgressTimer];
  299. // 完全停止播放并清理
  300. if (self.vodPlayer) {
  301. // 先停止播放
  302. [self.vodPlayer stopPlay];
  303. // 移除视图
  304. [self.vodPlayer removeVideoWidget];
  305. // 重置播放器配置,确保下次创建时是全新状态
  306. self.vodPlayer.vodDelegate = nil;
  307. // 释放播放器实例
  308. self.vodPlayer = nil;
  309. }
  310. // 清理视图
  311. if (self.playerView) {
  312. [self.playerView removeFromSuperview];
  313. self.playerView = nil;
  314. }
  315. // 重置状态
  316. [self updateState:JXSuperPlayerStateIdle];
  317. NSLog(@"[JXSuperPlayer] 播放器资源已完全释放");
  318. }
  319. - (void)dealloc {
  320. [self releasePlayer];
  321. NSLog(@"[JXSuperPlayer] dealloc");
  322. }
  323. #pragma mark - 工具方法
  324. + (NSString *)extractAppIdFromPsign:(NSString *)psign {
  325. if (!psign || psign.length == 0) {
  326. return nil;
  327. }
  328. // JWT格式: header.payload.signature
  329. NSArray *components = [psign componentsSeparatedByString:@"."];
  330. if (components.count < 2) {
  331. NSLog(@"[JXSuperPlayer] psign格式错误");
  332. return nil;
  333. }
  334. // 解析payload(Base64编码)
  335. NSString *payloadBase64 = components[1];
  336. // Base64解码
  337. NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:payloadBase64 options:0];
  338. if (!decodedData) {
  339. NSLog(@"[JXSuperPlayer] Base64解码失败");
  340. return nil;
  341. }
  342. // JSON解析
  343. NSError *error = nil;
  344. NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:decodedData
  345. options:0
  346. error:&error];
  347. if (error || ![payload isKindOfClass:[NSDictionary class]]) {
  348. NSLog(@"[JXSuperPlayer] JSON解析失败: %@", error);
  349. return nil;
  350. }
  351. // 提取appId
  352. id appIdValue = payload[@"appId"];
  353. if ([appIdValue isKindOfClass:[NSNumber class]]) {
  354. return [(NSNumber *)appIdValue stringValue];
  355. } else if ([appIdValue isKindOfClass:[NSString class]]) {
  356. return (NSString *)appIdValue;
  357. }
  358. NSLog(@"[JXSuperPlayer] psign中未找到appId");
  359. return nil;
  360. }
  361. #pragma mark - 错误处理和重试
  362. - (void)handlePlayError:(NSError *)error {
  363. // 检查是否是网络超时错误
  364. BOOL isNetworkTimeout = [error.domain isEqualToString:NSURLErrorDomain] &&
  365. (error.code == NSURLErrorTimedOut || error.code == NSURLErrorCannotConnectToHost);
  366. // 如果是网络错误且未达到最大重试次数,则重试
  367. if (isNetworkTimeout && self.retryCount < self.maxRetryCount) {
  368. self.retryCount++;
  369. self.isRetrying = YES;
  370. NSLog(@"[JXSuperPlayer] 播放失败,准备重试 (%ld/%ld): %@", (long)self.retryCount, (long)self.maxRetryCount, error.localizedDescription);
  371. // 延迟2秒后重试
  372. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  373. [self internalPlayWithParams:self.lastPlayParams];
  374. });
  375. } else {
  376. // 重试次数用完或非网络错误,直接通知错误
  377. if (self.retryCount >= self.maxRetryCount) {
  378. NSLog(@"[JXSuperPlayer] 重试次数用完,仍播放失败");
  379. }
  380. [self updateState:JXSuperPlayerStateError];
  381. [self notifyError:error];
  382. }
  383. }
  384. @end