| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- //
- // 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<NSKeyValueChangeKey,id> *)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
|