JXAVPlayer.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. //
  2. // JXAVPlayer.m
  3. // AICity
  4. //
  5. // Feature: 003-ios-api-https
  6. // 剧星AVPlayer封装类 - 实现文件
  7. //
  8. #import "JXAVPlayer.h"
  9. @interface JXAVPlayer ()
  10. @property (nonatomic, strong) AVPlayer *player;
  11. @property (nonatomic, strong) AVPlayerItem *playerItem;
  12. @property (nonatomic, strong) id timeObserver;
  13. @property (nonatomic, strong) NSTimer *progressTimer;
  14. @property (nonatomic, assign) BOOL isPlayerReady;
  15. @end
  16. @implementation JXAVPlayer
  17. // 进度上报间隔(30秒)
  18. static const NSTimeInterval kProgressReportInterval = 30.0;
  19. // 播放完成阈值(95%认为已完成)
  20. static const CGFloat kCompletionThreshold = 0.95;
  21. #pragma mark - Initialization
  22. - (instancetype)init {
  23. self = [super init];
  24. if (self) {
  25. [self setupPlayer];
  26. }
  27. return self;
  28. }
  29. - (void)dealloc {
  30. [self releasePlayer];
  31. }
  32. #pragma mark - Setup
  33. - (void)setupPlayer {
  34. // 创建AVPlayer
  35. self.player = [[AVPlayer alloc] init];
  36. self.isPlayerReady = NO;
  37. // 配置音频会话
  38. NSError *error = nil;
  39. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
  40. [[AVAudioSession sharedInstance] setActive:YES error:&error];
  41. if (error) {
  42. NSLog(@"Audio session setup error: %@", error.localizedDescription);
  43. }
  44. }
  45. #pragma mark - Playback Control
  46. - (void)playVideoWithURL:(NSString *)url
  47. episodeId:(NSString *)episodeId
  48. startPosition:(NSTimeInterval)startPosition {
  49. self.currentEpisodeId = episodeId;
  50. // 创建AVPlayerItem
  51. NSURL *videoURL = [NSURL URLWithString:url];
  52. self.playerItem = [AVPlayerItem playerItemWithURL:videoURL];
  53. // 添加playerItem观察者
  54. [self addPlayerItemObservers];
  55. // 替换当前播放项
  56. [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
  57. // 如果有起始位置,跳转到该位置
  58. if (startPosition > 0) {
  59. CMTime seekTime = CMTimeMakeWithSeconds(startPosition, NSEC_PER_SEC);
  60. [self.player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
  61. }
  62. // 开始播放
  63. [self.player play];
  64. // 开始进度跟踪
  65. [self startProgressTracking];
  66. }
  67. - (void)play {
  68. [self.player play];
  69. }
  70. - (void)pause {
  71. [self.player pause];
  72. }
  73. - (void)stop {
  74. [self.player pause];
  75. [self stopProgressTracking];
  76. }
  77. - (void)seekToPosition:(NSTimeInterval)position {
  78. CMTime seekTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
  79. [self.player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
  80. }
  81. #pragma mark - Playback Info
  82. - (NSTimeInterval)getCurrentPosition {
  83. CMTime currentTime = self.player.currentTime;
  84. if (CMTIME_IS_VALID(currentTime)) {
  85. return CMTimeGetSeconds(currentTime);
  86. }
  87. return 0.0;
  88. }
  89. - (NSTimeInterval)getDuration {
  90. if (self.playerItem && self.playerItem.duration.timescale != 0) {
  91. CMTime duration = self.playerItem.duration;
  92. if (CMTIME_IS_VALID(duration) && !CMTIME_IS_INDEFINITE(duration)) {
  93. return CMTimeGetSeconds(duration);
  94. }
  95. }
  96. return 0.0;
  97. }
  98. - (CGFloat)getProgress {
  99. NSTimeInterval duration = [self getDuration];
  100. if (duration <= 0) {
  101. return 0.0;
  102. }
  103. NSTimeInterval position = [self getCurrentPosition];
  104. CGFloat progress = position / duration;
  105. // 限制在 0.0 - 1.0 范围内
  106. return MAX(0.0, MIN(1.0, progress));
  107. }
  108. - (BOOL)isPlaying {
  109. return self.player.rate > 0;
  110. }
  111. - (BOOL)isPlaybackCompleted {
  112. return [self getProgress] >= kCompletionThreshold;
  113. }
  114. #pragma mark - Progress Tracking
  115. - (void)startProgressTracking {
  116. [self stopProgressTracking];
  117. // 创建定时器,每30秒上报一次进度
  118. self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:kProgressReportInterval
  119. target:self
  120. selector:@selector(reportProgress)
  121. userInfo:nil
  122. repeats:YES];
  123. }
  124. - (void)stopProgressTracking {
  125. if (self.progressTimer) {
  126. [self.progressTimer invalidate];
  127. self.progressTimer = nil;
  128. }
  129. }
  130. - (void)reportProgress {
  131. if (!self.currentEpisodeId) {
  132. return;
  133. }
  134. NSTimeInterval positionMs = [self getCurrentPosition] * 1000.0;
  135. NSTimeInterval durationMs = [self getDuration] * 1000.0;
  136. CGFloat progress = [self getProgress];
  137. BOOL isCompleted = [self isPlaybackCompleted];
  138. if ([self.progressDelegate respondsToSelector:@selector(playerDidUpdateProgress:positionMs:durationMs:progress:isCompleted:)]) {
  139. [self.progressDelegate playerDidUpdateProgress:self.currentEpisodeId
  140. positionMs:positionMs
  141. durationMs:durationMs
  142. progress:progress
  143. isCompleted:isCompleted];
  144. }
  145. }
  146. - (void)reportCurrentProgress {
  147. [self reportProgress];
  148. }
  149. #pragma mark - Observer Management
  150. - (void)addPlayerItemObservers {
  151. // 观察播放器状态
  152. [self.playerItem addObserver:self
  153. forKeyPath:@"status"
  154. options:NSKeyValueObservingOptionNew
  155. context:nil];
  156. // 观察缓冲状态
  157. [self.playerItem addObserver:self
  158. forKeyPath:@"playbackBufferEmpty"
  159. options:NSKeyValueObservingOptionNew
  160. context:nil];
  161. [self.playerItem addObserver:self
  162. forKeyPath:@"playbackLikelyToKeepUp"
  163. options:NSKeyValueObservingOptionNew
  164. context:nil];
  165. // 监听播放结束通知
  166. [[NSNotificationCenter defaultCenter] addObserver:self
  167. selector:@selector(playerItemDidReachEnd:)
  168. name:AVPlayerItemDidPlayToEndTimeNotification
  169. object:self.playerItem];
  170. // 监听播放失败通知
  171. [[NSNotificationCenter defaultCenter] addObserver:self
  172. selector:@selector(playerItemFailedToPlayToEnd:)
  173. name:AVPlayerItemFailedToPlayToEndTimeNotification
  174. object:self.playerItem];
  175. }
  176. - (void)removePlayerItemObservers {
  177. if (!self.playerItem) {
  178. return;
  179. }
  180. @try {
  181. [self.playerItem removeObserver:self forKeyPath:@"status"];
  182. [self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
  183. [self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
  184. } @catch (NSException *exception) {
  185. NSLog(@"Exception removing observer: %@", exception);
  186. }
  187. [[NSNotificationCenter defaultCenter] removeObserver:self
  188. name:AVPlayerItemDidPlayToEndTimeNotification
  189. object:self.playerItem];
  190. [[NSNotificationCenter defaultCenter] removeObserver:self
  191. name:AVPlayerItemFailedToPlayToEndTimeNotification
  192. object:self.playerItem];
  193. }
  194. #pragma mark - KVO
  195. - (void)observeValueForKeyPath:(NSString *)keyPath
  196. ofObject:(id)object
  197. change:(NSDictionary<NSKeyValueChangeKey,id> *)change
  198. context:(void *)context {
  199. if (object == self.playerItem) {
  200. if ([keyPath isEqualToString:@"status"]) {
  201. AVPlayerItemStatus status = self.playerItem.status;
  202. switch (status) {
  203. case AVPlayerItemStatusReadyToPlay:
  204. // 准备就绪
  205. if (!self.isPlayerReady) {
  206. self.isPlayerReady = YES;
  207. if ([self.delegate respondsToSelector:@selector(playerDidReady)]) {
  208. [self.delegate playerDidReady];
  209. }
  210. }
  211. break;
  212. case AVPlayerItemStatusFailed:
  213. // 播放失败
  214. [self handlePlayerError:self.playerItem.error];
  215. break;
  216. case AVPlayerItemStatusUnknown:
  217. // 未知状态
  218. break;
  219. }
  220. }
  221. else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
  222. // 缓冲区为空,需要缓冲
  223. if (self.playerItem.playbackBufferEmpty) {
  224. if ([self.delegate respondsToSelector:@selector(playerDidBuffering)]) {
  225. [self.delegate playerDidBuffering];
  226. }
  227. }
  228. }
  229. else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
  230. // 缓冲充足,可以播放
  231. if (self.playerItem.playbackLikelyToKeepUp) {
  232. if ([self.delegate respondsToSelector:@selector(playerDidStartPlaying)]) {
  233. [self.delegate playerDidStartPlaying];
  234. }
  235. }
  236. }
  237. }
  238. }
  239. #pragma mark - Notifications
  240. - (void)playerItemDidReachEnd:(NSNotification *)notification {
  241. [self stopProgressTracking];
  242. if ([self.delegate respondsToSelector:@selector(playerDidFinishPlaying)]) {
  243. [self.delegate playerDidFinishPlaying];
  244. }
  245. }
  246. - (void)playerItemFailedToPlayToEnd:(NSNotification *)notification {
  247. NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey];
  248. [self handlePlayerError:error];
  249. }
  250. #pragma mark - Error Handling
  251. - (void)handlePlayerError:(NSError *)error {
  252. [self stopProgressTracking];
  253. NSLog(@"Player error: %@", error.localizedDescription);
  254. if ([self.delegate respondsToSelector:@selector(playerDidFailWithError:)]) {
  255. [self.delegate playerDidFailWithError:error];
  256. }
  257. }
  258. #pragma mark - Cleanup
  259. - (void)releasePlayer {
  260. [self stopProgressTracking];
  261. [self reportCurrentProgress]; // 释放前最后一次上报进度
  262. [self removePlayerItemObservers];
  263. [self.player pause];
  264. [self.player replaceCurrentItemWithPlayerItem:nil];
  265. self.playerItem = nil;
  266. self.player = nil;
  267. self.isPlayerReady = NO;
  268. self.currentEpisodeId = nil;
  269. }
  270. @end