// // JXAVPlayer.m // AICity // // Feature: 003-ios-api-https // 剧星AVPlayer封装类 - 实现文件 // #import "JXAVPlayer.h" @interface JXAVPlayer () @property (nonatomic, strong) AVPlayer *player; @property (nonatomic, strong) AVPlayerItem *playerItem; @property (nonatomic, strong) id timeObserver; @property (nonatomic, strong) NSTimer *progressTimer; @property (nonatomic, assign) BOOL isPlayerReady; @end @implementation JXAVPlayer // 进度上报间隔(30秒) static const NSTimeInterval kProgressReportInterval = 30.0; // 播放完成阈值(95%认为已完成) static const CGFloat kCompletionThreshold = 0.95; #pragma mark - Initialization - (instancetype)init { self = [super init]; if (self) { [self setupPlayer]; } return self; } - (void)dealloc { [self releasePlayer]; } #pragma mark - Setup - (void)setupPlayer { // 创建AVPlayer self.player = [[AVPlayer alloc] init]; self.isPlayerReady = NO; // 配置音频会话 NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; if (error) { NSLog(@"Audio session setup error: %@", error.localizedDescription); } } #pragma mark - Playback Control - (void)playVideoWithURL:(NSString *)url episodeId:(NSString *)episodeId startPosition:(NSTimeInterval)startPosition { self.currentEpisodeId = episodeId; // 创建AVPlayerItem NSURL *videoURL = [NSURL URLWithString:url]; self.playerItem = [AVPlayerItem playerItemWithURL:videoURL]; // 添加playerItem观察者 [self addPlayerItemObservers]; // 替换当前播放项 [self.player replaceCurrentItemWithPlayerItem:self.playerItem]; // 如果有起始位置,跳转到该位置 if (startPosition > 0) { CMTime seekTime = CMTimeMakeWithSeconds(startPosition, NSEC_PER_SEC); [self.player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; } // 开始播放 [self.player play]; // 开始进度跟踪 [self startProgressTracking]; } - (void)play { [self.player play]; } - (void)pause { [self.player pause]; } - (void)stop { [self.player pause]; [self stopProgressTracking]; } - (void)seekToPosition:(NSTimeInterval)position { CMTime seekTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC); [self.player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; } #pragma mark - Playback Info - (NSTimeInterval)getCurrentPosition { CMTime currentTime = self.player.currentTime; if (CMTIME_IS_VALID(currentTime)) { return CMTimeGetSeconds(currentTime); } return 0.0; } - (NSTimeInterval)getDuration { if (self.playerItem && self.playerItem.duration.timescale != 0) { CMTime duration = self.playerItem.duration; if (CMTIME_IS_VALID(duration) && !CMTIME_IS_INDEFINITE(duration)) { return CMTimeGetSeconds(duration); } } return 0.0; } - (CGFloat)getProgress { NSTimeInterval duration = [self getDuration]; if (duration <= 0) { return 0.0; } NSTimeInterval position = [self getCurrentPosition]; CGFloat progress = position / duration; // 限制在 0.0 - 1.0 范围内 return MAX(0.0, MIN(1.0, progress)); } - (BOOL)isPlaying { return self.player.rate > 0; } - (BOOL)isPlaybackCompleted { return [self getProgress] >= kCompletionThreshold; } #pragma mark - Progress Tracking - (void)startProgressTracking { [self stopProgressTracking]; // 创建定时器,每30秒上报一次进度 self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:kProgressReportInterval target:self selector:@selector(reportProgress) userInfo:nil repeats:YES]; } - (void)stopProgressTracking { if (self.progressTimer) { [self.progressTimer invalidate]; self.progressTimer = nil; } } - (void)reportProgress { if (!self.currentEpisodeId) { return; } NSTimeInterval positionMs = [self getCurrentPosition] * 1000.0; NSTimeInterval durationMs = [self getDuration] * 1000.0; CGFloat progress = [self getProgress]; BOOL isCompleted = [self isPlaybackCompleted]; if ([self.progressDelegate respondsToSelector:@selector(playerDidUpdateProgress:positionMs:durationMs:progress:isCompleted:)]) { [self.progressDelegate playerDidUpdateProgress:self.currentEpisodeId positionMs:positionMs durationMs:durationMs progress:progress isCompleted:isCompleted]; } } - (void)reportCurrentProgress { [self reportProgress]; } #pragma mark - Observer Management - (void)addPlayerItemObservers { // 观察播放器状态 [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; // 观察缓冲状态 [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil]; [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil]; // 监听播放结束通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; // 监听播放失败通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemFailedToPlayToEnd:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:self.playerItem]; } - (void)removePlayerItemObservers { if (!self.playerItem) { return; } @try { [self.playerItem removeObserver:self forKeyPath:@"status"]; [self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; [self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; } @catch (NSException *exception) { NSLog(@"Exception removing observer: %@", exception); } [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:self.playerItem]; } #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == self.playerItem) { if ([keyPath isEqualToString:@"status"]) { AVPlayerItemStatus status = self.playerItem.status; switch (status) { case AVPlayerItemStatusReadyToPlay: // 准备就绪 if (!self.isPlayerReady) { self.isPlayerReady = YES; if ([self.delegate respondsToSelector:@selector(playerDidReady)]) { [self.delegate playerDidReady]; } } break; case AVPlayerItemStatusFailed: // 播放失败 [self handlePlayerError:self.playerItem.error]; break; case AVPlayerItemStatusUnknown: // 未知状态 break; } } else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) { // 缓冲区为空,需要缓冲 if (self.playerItem.playbackBufferEmpty) { if ([self.delegate respondsToSelector:@selector(playerDidBuffering)]) { [self.delegate playerDidBuffering]; } } } else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) { // 缓冲充足,可以播放 if (self.playerItem.playbackLikelyToKeepUp) { if ([self.delegate respondsToSelector:@selector(playerDidStartPlaying)]) { [self.delegate playerDidStartPlaying]; } } } } } #pragma mark - Notifications - (void)playerItemDidReachEnd:(NSNotification *)notification { [self stopProgressTracking]; if ([self.delegate respondsToSelector:@selector(playerDidFinishPlaying)]) { [self.delegate playerDidFinishPlaying]; } } - (void)playerItemFailedToPlayToEnd:(NSNotification *)notification { NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey]; [self handlePlayerError:error]; } #pragma mark - Error Handling - (void)handlePlayerError:(NSError *)error { [self stopProgressTracking]; NSLog(@"Player error: %@", error.localizedDescription); if ([self.delegate respondsToSelector:@selector(playerDidFailWithError:)]) { [self.delegate playerDidFailWithError:error]; } } #pragma mark - Cleanup - (void)releasePlayer { [self stopProgressTracking]; [self reportCurrentProgress]; // 释放前最后一次上报进度 [self removePlayerItemObservers]; [self.player pause]; [self.player replaceCurrentItemWithPlayerItem:nil]; self.playerItem = nil; self.player = nil; self.isPlayerReady = NO; self.currentEpisodeId = nil; } @end