/**
* FVPVideoPlayer.m
*
* 视频播放器核心实现类
*
* 功能概述:
* - 基于AVFoundation框架实现的视频播放器
* - 支持本地和网络视频播放
* - 提供完整的播放控制功能(播放、暂停、跳转、音量、播放速度等)
* - 支持视频旋转和变换处理
* - 实现KVO观察者模式监听播放状态变化
* - 提供事件回调机制与Flutter层通信
*
* 主要职责:
* 1. 视频播放器生命周期管理
* 2. 播放状态监控和事件分发
* 3. 视频输出和渲染配置
* 4. 播放参数控制(音量、速度、循环等)
* 5. 错误处理和异常管理
*
* 核心组件:
* - AVPlayer: 底层播放器实例
* - AVPlayerItem: 播放项目和媒体资源
* - AVPlayerItemVideoOutput: 视频输出配置
* - FVPVideoEventListener: 事件监听器接口
*
*/
FVPVideoPlayer
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
avFactory:(id <FVPAVFactory>)avFactory
viewProvider:(NSObject <FVPViewProvider> *)viewProvider {
self = [super init];
NSAssert(self, @"super init cannot be nil");
_viewProvider = viewProvider;
AVAsset *asset = [item asset];
void (^assetCompletionHandler)(void) = ^{
NSLog(@"🩷 FVPVideoPlayer: 开始处理资源轨道信息");
if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) {
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if ([tracks count] > 0) {
NSLog(@"🩷 FVPVideoPlayer: 发现视频轨道,开始处理变换信息");
AVAssetTrack *videoTrack = tracks[0];
void (^trackCompletionHandler)(void) = ^{
if (self->_disposed) return;
if ([videoTrack statusOfValueForKey:@"preferredTransform"
error:nil] == AVKeyValueStatusLoaded) {
NSLog(@"🩷 FVPVideoPlayer: 应用视频变换矩阵");
// Rotate the video by using a videoComposition and the preferredTransform
self->_preferredTransform = FVPGetStandardizedTransformForTrack(videoTrack);
// Do not use video composition when it is not needed.
if (CGAffineTransformIsIdentity(self->_preferredTransform)) {
NSLog(@"🩷 FVPVideoPlayer: 视频无需变换,使用原始方向");
return;
}
NSLog(@"🩷 FVPVideoPlayer: 创建视频合成配置");
// Note:
// https://developer.apple.com/documentation/avfoundation/avplayeritem/1388818-videocomposition
// Video composition can only be used with file-based media and is not supported for
// use with media served using HTTP Live Streaming.
AVMutableVideoComposition *videoComposition =
[self getVideoCompositionWithTransform:self->_preferredTransform
withAsset:asset
withVideoTrack:videoTrack];
item.videoComposition = videoComposition;
NSLog(@"🩷 FVPVideoPlayer: 视频合成配置应用完成");
}
};
[videoTrack loadValuesAsynchronouslyForKeys:@[@"preferredTransform"]
completionHandler:trackCompletionHandler];
}
}
};
NSLog(@"🩷 FVPVideoPlayer: 创建AVPlayer实例");
_player = [avFactory playerWithPlayerItem:item];
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
NSLog(@"🩷 FVPVideoPlayer: 配置视频输出格式");
// Configure output.
NSDictionary *pixBuffAttributes = @{
(id) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
(id) kCVPixelBufferIOSurfacePropertiesKey: @{}
};
_videoOutput = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes];
NSLog(@"🩷 FVPVideoPlayer: 开始异步加载资源轨道");
[asset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:assetCompletionHandler];
return self;
}
视频初始化完成后, 会立即调用setEventListener方法设置事件监听器。
setEventListener
- (void)setEventListener:(NSObject <FVPVideoEventListener> *)eventListener {
_eventListener = eventListener;
// The first time an event listener is set, set up video event listeners to relay status changes
// 第一次设置事件监听器时,设置视频事件监听器以中继状态变化
// changes to the event listener.
// 变化到事件监听器。
if (eventListener && !_listenersRegistered) {
AVPlayerItem *item = self.player.currentItem;
// If the item is already ready to play, ensure that the intialized event is sent first.
// 如果项目已经准备好播放,确保首先发送初始化事件。
[self reportStatusForPlayerItem:item];
// Set up all necessary observers to report video events.
// 设置所有必要的观察者以报告视频事件。
NSLog(@"🩷 FVPVideoPlayer: 注册播放项KVO观察者");
FVPRegisterKeyValueObservers(self, FVPGetPlayerItemObservations(), item);
NSLog(@"🩷 FVPVideoPlayer: 注册播放器KVO观察者");
FVPRegisterKeyValueObservers(self, FVPGetPlayerObservations(), _player);
NSLog(@"🩷 FVPVideoPlayer: 注册播放结束通知监听器");
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(itemDidPlayToEndTime:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:item];
_listenersRegistered = YES;
}
}
这里首先会调用reportStatusForPlayerItem方法如果项目已经准备好播放,确保首先发送初始化事件。
然后添加注册播放项KVO观察者、注册播放器KVO观察者、注册播放结束通知监听器。
reportStatusForPlayerItem
- (void)reportStatusForPlayerItem:(AVPlayerItem *)item {
NSLog(@"🩷 FVPVideoPlayer: 播放项状态: %ld", (long) item.status);
switch (item.status) {
case AVPlayerItemStatusFailed:
NSLog(@"🩷 FVPVideoPlayer: 播放项状态为失败,发送加载失败事件");
[self sendFailedToLoadVideoEvent];
break;
case AVPlayerItemStatusUnknown:
NSLog(@"🩷 FVPVideoPlayer: 播放项状态未知,等待状态更新");
break;
case AVPlayerItemStatusReadyToPlay:
[item addOutput:_videoOutput];
NSLog(@"🩷 FVPVideoPlayer: 视频输出已添加,检查初始化状态");
[self reportInitializedIfReadyToPlay];
break;
}
}
* 报告播放项状态
*
* @param item 播放项对象
*
* 功能说明:
* 根据播放项的状态执行相应的处理逻辑
* - Failed: 发送加载失败事件
* - Unknown: 暂不处理,等待状态更新
* - ReadyToPlay: 添加视频输出并检查是否可以报告初始化完成
FVPGetPlayerItemObservations
/// Returns a mapping of KVO keys to NSValue-wrapped observer context pointers for observations that
/// 返回KVO键到NSValue包装的观察者上下文指针的映射,用于应该为AVPlayerItem实例设置的观察。
/// should be set for AVPlayerItem instances.
/// 应该为AVPlayerItem实例设置的观察。
static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
return @{
@"loadedTimeRanges": [NSValue valueWithPointer:timeRangeContext],
@"status": [NSValue valueWithPointer:statusContext],
@"presentationSize": [NSValue valueWithPointer:presentationSizeContext],
@"duration": [NSValue valueWithPointer:durationContext],
@"playbackLikelyToKeepUp": [NSValue valueWithPointer:playbackLikelyToKeepUpContext],
};
}
FVPGetPlayerObservations
/// Returns a mapping of KVO keys to NSValue-wrapped observer context pointers for observations that
/// 返回KVO键到NSValue包装的观察者上下文指针的映射,用于应该为AVPlayer实例设置的观察。
/// should be set for AVPlayer instances.
/// 应该为AVPlayer实例设置的观察。
static NSDictionary<NSString *, NSValue *> *FVPGetPlayerObservations(void) {
return @{
@"rate": [NSValue valueWithPointer:rateContext],
};
}
所有观察者注册完成后, 观察者的回调方法就会不断接收到监听属性的回调。开发者在监听到的回调里处理逻辑。
(void)observeValueForKeyPath:(NSString *)path ofObject:(id)object change: context:
* KVO观察者回调方法
*
* @param path 被观察的属性路径
* @param object 被观察的对象
* @param change 属性变化信息
* @param context 观察者上下文标识
duration、presentationSize变化
if (context == presentationSizeContext || context == durationContext) {
// 处理视频尺寸或时长变化-presentationSize/duration
NSLog(@"🩷 FVPVideoPlayer: 处理视频尺寸或时长变化");
AVPlayerItem *item = (AVPlayerItem *) object;
if (item.status == AVPlayerItemStatusReadyToPlay) {
NSLog(@"🩷 FVPVideoPlayer: 播放项已准备就绪,检查初始化状态");
// Due to an apparent bug, when the player item is ready, it still may not have determined
// 由于一个明显的bug,当播放项准备就绪时,它可能仍然没有确定
// its presentation size or duration. When these properties are finally set, re-check if
// 它的显示尺寸或时长。当这些属性最终设置时,重新检查是否
// all required properties and instantiate the event sink if it is not already set up.
// 所有必需的属性都已设置,如果事件接收器尚未设置,则实例化它。
[self reportInitializedIfReadyToPlay];
} else {
NSLog(@"🩷 FVPVideoPlayer: 播放项尚未准备就绪,状态: %ld", (long) item.status);
}
}
KVO首先监听到的是视频时长duration和尺寸presentationSize的变化。
当前播放项AVPlayerItem的状态是AVPlayerItemStatusUnknown、AVPlayerItemStatusFailed时, 控制台只打印日志,标记KVO回调处理完成。
当前播放项AVPlayerItem已经准备就绪状态AVPlayerItemStatusReadyToPlay下, 立即调用reportInitializedIfReadyToPlay 报告初始化完成。
loadedTimeRanges
紧接着KVO持续接收到loadedTimeRanges缓冲时间范围变化的通知回调。原生端在这个回调里,将缓冲时间范围持续发送给flutter,通过调用EventBridge里的videoPlayerDidUpdateBufferRegions方法。
视频缓冲区更新事件:
[self sendOrQueue:@{@"event": @"bufferingUpdate", @"values": regions}];
AVFoundationVideoPlayer-bufferingUpdate
flutter端接收到bufferingUpdate事件后, 将缓冲数据包装成DurationRange对象放到数组里。
Stream<VideoEvent> videoEventsFor(int playerId) {
debugPrint('---✅dart启动视频播放器流监听videoEventsFor, playerId: $playerId✅---');
return _eventChannelFor(playerId).receiveBroadcastStream().map((dynamic event) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
debugPrint('---✅dart视频播放器流监听videoEventsFor结果, map: $map✅---');
return switch (map['event']) {
'bufferingUpdate' => VideoEvent(
buffered: (map['values'] as List<dynamic>).map<DurationRange>(_toDurationRange).toList(),
eventType: VideoEventType.bufferingUpdate,
)
};
});
}
playbackLikelyToKeepUp
else if (context == playbackLikelyToKeepUpContext) {
// 处理缓冲可能性变化-playbackLikelyToKeepUp
NSLog(@"🩷 FVPVideoPlayer: 处理缓冲可能性变化");
[self updatePlayingState];
if ([[_player currentItem] isPlaybackLikelyToKeepUp]) {
NSLog(@"🩷 FVPVideoPlayer: 缓冲充足,结束缓冲状态");
[self.eventListener videoPlayerDidEndBuffering];
} else {
NSLog(@"🩷 FVPVideoPlayer: 缓冲不足,开始缓冲状态");
[self.eventListener videoPlayerDidStartBuffering];
}
}
当监听到缓冲冲可能性变化时, 原生端调用updatePlayingState更新播放状态(如果视频播放器已经初始化的情况下)。
缓冲可能性变化回调会持续执行。
当视频缓冲不足时, 向flutter端发送开始缓冲的消息,videoPlayerDidStartBuffering 和videoPlayerDidEndBuffering消息会持续发送。
从日志可以看到, 缓冲不足、缓冲充足的回调会持续执行。
updatePlayingState
- (void)updatePlayingState {
NSLog(@"🩷 FVPVideoPlayer: 播放器初始化状态: %@", _isInitialized ? @"已初始化" : @"未初始化");
if (!_isInitialized) {
NSLog(@"🩷 FVPVideoPlayer: 播放器未初始化,跳过状态更新");
return;
}
NSLog(@"🩷 FVPVideoPlayer: 当前播放状态标志: %@", _isPlaying ? @"应该播放" : @"应该暂停");
if (_isPlaying) {
// Calling play is the same as setting the rate to 1.0 (or to defaultRate depending on iOS
// 调用play等同于设置rate为1.0(或根据iOS版本设置为defaultRate)
// version) so last set playback speed must be set here if any instead.
// 因此如果设置了播放速度,必须在这里调用updateRate而不是play
// https://github.com/flutter/flutter/issues/71264
// https://github.com/flutter/flutter/issues/73643
if (_targetPlaybackSpeed) {
NSLog(@"🩷 FVPVideoPlayer: 检测到目标播放速度: %.2fx,调用updateRate",
_targetPlaybackSpeed.floatValue);
[self updateRate];
} else {
NSLog(@"🩷 FVPVideoPlayer: 无特定播放速度,直接调用play");
[_player play];
}
NSLog(@"🩷 FVPVideoPlayer: 播放操作已执行");
} else {
[_player pause];
NSLog(@"🩷 FVPVideoPlayer: 暂停操作已执行");
}
NSLog(@"🩷 FVPVideoPlayer: 播放状态更新完成");
}
更新播放状态。 刚开始缓冲一段时间, 视频播放器并没有初始化成功, 所以会根据_isInitialized字段停止掉,不继续执行真正更新播放状态的代码逻辑。
默认播放状态_isPlaying为false, 也就是未播放状态。_isPlaying状态的变更由用户在flutter端手动调用play方法触发。
缓冲阶段_isPlaying为false, 那么会一直执行[_player pause]暂停播放。
缓冲阶段用户在flutter端点击播放按钮,变更播放状态_isPlaying从false成为true, 那么会在原生端调用[_player play]开始播放视频。并且缓冲阶段继续执行。
AVFoundationVideoPlayer-bufferingStart
Stream<VideoEvent> videoEventsFor(int playerId) {
debugPrint('---✅dart启动视频播放器流监听videoEventsFor, playerId: $playerId✅---');
return _eventChannelFor(playerId).receiveBroadcastStream().map((dynamic event) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
debugPrint('---✅dart视频播放器流监听videoEventsFor结果, map: $map✅---');
return switch (map['event']) {
'bufferingStart' => VideoEvent(eventType: VideoEventType.bufferingStart),
};
});
}
flutter端持续接收到缓冲开始消息, 封装VideoEvent状态对象, 并标识播放状态为VideoEventType.bufferingStart。
AVFoundationVideoPlayer-videoPlayerDidEndBuffering
Stream<VideoEvent> videoEventsFor(int playerId) {
debugPrint('---✅dart启动视频播放器流监听videoEventsFor, playerId: $playerId✅---');
return _eventChannelFor(playerId).receiveBroadcastStream().map((dynamic event) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
debugPrint('---✅dart视频播放器流监听videoEventsFor结果, map: $map✅---');
return switch (map['event']) {
'videoPlayerDidEndBuffering' => VideoEvent(eventType: VideoEventType.videoPlayerDidEndBuffering),
};
});
}
flutter端持续接收到缓冲开始消息, 封装VideoEvent状态对象, 并标识播放状态为VideoEventType.videoPlayerDidEndBuffering。
AVFoundationVideoPlayer-status
if (context == statusContext) {
// 处理播放项状态变化-status
NSLog(@"🩷 FVPVideoPlayer: 处理播放项状态变化");
AVPlayerItem *item = (AVPlayerItem *) object;
NSLog(@"🩷 FVPVideoPlayer: 播放项状态: %ld", (long) item.status);
[self reportStatusForPlayerItem:item];
}
KVO监听到处理播放项状态变化时, 会报告播放项状态。
reportStatusForPlayerItem-报告播放项状态
- (void)reportStatusForPlayerItem:(AVPlayerItem *)item {
NSLog(@"🩷 FVPVideoPlayer: 播放项状态: %ld", (long) item.status);
switch (item.status) {
case AVPlayerItemStatusFailed:
NSLog(@"🩷 FVPVideoPlayer: 播放项状态为失败,发送加载失败事件");
[self sendFailedToLoadVideoEvent];
break;
case AVPlayerItemStatusUnknown:
NSLog(@"🩷 FVPVideoPlayer: 播放项状态未知,等待状态更新");
break;
case AVPlayerItemStatusReadyToPlay:
[item addOutput:_videoOutput];
NSLog(@"🩷 FVPVideoPlayer: 视频输出已添加,检查初始化状态");
[self reportInitializedIfReadyToPlay];
break;
}
NSLog(@"🩷 FVPVideoPlayer: 播放项状态报告完成");
}
播放项状态为失败就发送失败事件, 播放项状态未知控制台打印日志。 播放状态AVPlayerItemStatusReadyToPlay准备播放时发送初始化成功事件
reportInitializedIfReadyToPlay-根据播放时长duration决定是否发送初始化成功事件
- (void)reportInitializedIfReadyToPlay {
NSLog(@"🩷 FVPVideoPlayer: 当前初始化状态: %@", _isInitialized ? @"已初始化" : @"未初始化");
if (!_isInitialized) {
AVPlayerItem *currentItem = self.player.currentItem;
CGSize size = currentItem.presentationSize;
CGFloat width = size.width;
CGFloat height = size.height;
NSLog(@"🩷 FVPVideoPlayer: 视频尺寸: %.0fx%.0f", width, height);
// Wait until tracks are loaded to check duration or if there are any videos.
// 等待轨道加载完成以检查时长或是否有视频。
AVAsset *asset = currentItem.asset;
if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) {
NSLog(@"🩷 FVPVideoPlayer: 轨道信息未加载完成,异步加载轨道信息");
void (^trackCompletionHandler)(void) = ^{
if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) {
// Cancelled, or something failed.
// 已取消,或出现错误。
NSLog(@"🩷 FVPVideoPlayer: 轨道加载失败或被取消");
return;
}
// This completion block will run on an AVFoundation background queue.
// 此完成块将在AVFoundation后台队列上运行。
// Hop back to the main thread to set up event sink.
// 跳回主线程设置事件接收器。
NSLog(@"🩷 FVPVideoPlayer: 轨道加载完成,切换到主线程继续处理");
[self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO];
};
[asset loadValuesAsynchronouslyForKeys:@[@"tracks"]
completionHandler:trackCompletionHandler];
return;
}
NSLog(@"🩷 FVPVideoPlayer: 轨道信息已加载,开始分析轨道类型");
BOOL hasVideoTracks = [asset tracksWithMediaType:AVMediaTypeVideo].count != 0;
// Audio-only HLS files have no size, so `currentItem.tracks.count` must be used to check for
// 纯音频HLS文件没有尺寸,因此必须使用`currentItem.tracks.count`来检查
// track presence, as AVAsset does not always provide track information in HLS streams.
// 轨道存在,因为AVAsset在HLS流中并不总是提供轨道信息。
BOOL hasNoTracks = currentItem.tracks.count == 0 && asset.tracks.count == 0;
NSLog(@"🩷 FVPVideoPlayer: 视频轨道数量: %lu",
(unsigned long) [asset tracksWithMediaType:AVMediaTypeVideo].count);
NSLog(@"🩷 FVPVideoPlayer: 播放项轨道数量: %lu", (unsigned long) currentItem.tracks.count);
NSLog(@"🩷 FVPVideoPlayer: 资源轨道数量: %lu", (unsigned long) asset.tracks.count);
NSLog(@"🩷 FVPVideoPlayer: 有视频轨道: %@", hasVideoTracks ? @"是" : @"否");
NSLog(@"🩷 FVPVideoPlayer: 无轨道: %@", hasNoTracks ? @"是" : @"否");
// The player has not yet initialized when it has no size, unless it is an audio-only track.
// 播放器在没有尺寸时尚未初始化,除非它是纯音频轨道。
// HLS m3u8 video files never load any tracks, and are also not yet initialized until they have
// HLS m3u8视频文件从不加载任何轨道,并且在有尺寸之前也不会初始化
// a size.
// 尺寸。
if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height &&
width == CGSizeZero.width) {
NSLog(@"🩷 FVPVideoPlayer: 视频文件但尺寸为零,等待尺寸信息");
return;
}
// The player may be initialized but still needs to determine the duration.
// 播放器可能已初始化但仍需要确定时长。
int64_t duration = [self duration];
NSLog(@"🩷 FVPVideoPlayer: 视频时长: %lld毫秒", duration);
if (duration == 0) {
NSLog(@"🩷 FVPVideoPlayer: 时长为零,等待时长信息");
return;
}
NSLog(@"🩷 FVPVideoPlayer: 所有初始化条件已满足,设置初始化状态");
_isInitialized = YES;
NSLog(@"🩷 FVPVideoPlayer: 更新播放状态");
[self updatePlayingState];
// 发送初始化成功消息给dart
NSLog(@"🩷 FVPVideoPlayer: 发送初始化完成事件,时长: %lld毫秒,尺寸: %.0fx%.0f", duration,
size.width, size.height);
[self.eventListener videoPlayerDidInitializeWithDuration:duration size:size];
NSLog(@"🩷 FVPVideoPlayer: 初始化完成事件已发送");
} else {
NSLog(@"🩷 FVPVideoPlayer: 播放器已初始化,跳过重复初始化");
}
NSLog(@"🩷 FVPVideoPlayer: 初始化检查完成");
}
AVFoundationVideoPlayer-rate决定发送给flutter端的播放状态(特别重要)
if (context == rateContext) {
// 处理播放速率变化-rate
NSLog(@"🩷 FVPVideoPlayer: 处理播放速率变化");
// Important: Make sure to cast the object to AVPlayer when observing the rate property,
// 重要:观察rate属性时,确保将对象转换为AVPlayer,
// as it is not available in AVPlayerItem.
// 因为它在AVPlayerItem中不可用。
AVPlayer *player = (AVPlayer *) object;
BOOL isPlaying = player.rate > 0;
NSLog(@"🩷 FVPVideoPlayer: 播放速率: %.2f,播放状态: %@", player.rate,
isPlaying ? @"播放中" : @"已暂停");
[self.eventListener videoPlayerDidSetPlaying:isPlaying];
}
缓冲阶段, 默认播放速率是0, 所以播放状态isPlaying为false。向flutter端发送
[self sendOrQueue:@{@"event": @"isPlayingStateUpdate", @"isPlaying": @(playing)}];
事件。playing为false。
当用户在flutter端点击播放按钮调用play方法, 原生KVO的速率回调会执行,并检测到速率rate>0, 接着发送播放状态isPlaying为true给flutter端。
AVFoundationVideoPlayer-playWithError
/**
* 开始播放视频
*
* @param error 错误信息输出参数
*
* 功能说明:
* 设置播放状态为YES,并更新播放器的实际播放状态
*/
- (void)playWithError:(FlutterError *_Nullable*_Nonnull)error {
NSLog(@"🩷 FVPVideoPlayer: 开始播放视频");
_isPlaying = YES;
[self updatePlayingState];
NSLog(@"🩷 FVPVideoPlayer: 播放状态已更新");
}
用户在flutter端点击播放按钮, 调用上面的播放方法并标记_isPlaying为true,
然后播放速率的KVO回调会监听到rate, 根据是否rate>0发送开始播放事件。
当用户在flutter端手动暂停时, 播放速率的KVO回调会监听到rate=0,就会发送暂停播放事件。
AVFoundationVideoPlayer-pauseWithError
/**
* 暂停播放视频
*
* @param error 错误信息输出参数
*
* 功能说明:
* 设置播放状态为NO,并更新播放器的实际播放状态
*/
- (void)pauseWithError:(FlutterError *_Nullable*_Nonnull)error {
NSLog(@"🩷 FVPVideoPlayer: 暂停播放视频");
_isPlaying = NO;
[self updatePlayingState];
NSLog(@"🩷 FVPVideoPlayer: 暂停状态已更新");
}
可以看到, 发送给flutter端的播放状态是根据KVO监听到的播放速率变化决定的。
AVFoundationVideoPlayer-position
/**
* 获取当前播放位置
*
* @param error 错误信息输出参数
* @return 当前播放位置(毫秒)
*
* 功能说明:
* 返回播放器当前的播放时间位置,以毫秒为单位
*/
- (nullable NSNumber*)position:(FlutterError *_Nullable*_Nonnull)error {
int64_t currentPosition = FVPCMTimeToMillis([_player currentTime]);
NSLog(@"🩷 FVPVideoPlayer: 获取当前播放位置: %lld毫秒", currentPosition);
return @(currentPosition);
}
获取当前播放位置。
AVFoundationVideoPlayer-seekTo
/**
* 跳转到指定播放位置
*
* @param position 目标位置(毫秒)
* @param completion 跳转完成回调
*
* 功能说明:
* 将播放器跳转到指定的时间位置,支持精确跳转和容差处理
*/
- (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable))completion {
NSLog(@"🩷 FVPVideoPlayer: 开始跳转到位置: %ld毫秒", (long) position);
CMTime targetCMTime = CMTimeMake(position, 1000);
CMTimeValue duration = _player.currentItem.asset.duration.value;
// Without adding tolerance when seeking to duration,
// seekToTime will never complete, and this call will hang.
// see issue https://github.com/flutter/flutter/issues/124475.
CMTime tolerance = position == duration ? CMTimeMake(1, 1000) : kCMTimeZero;
[_player seekToTime:targetCMTime
toleranceBefore:tolerance
toleranceAfter:tolerance
completionHandler:^(BOOL completed) {
NSLog(@"🩷 FVPVideoPlayer: 跳转完成,成功: %@", completed ? @"是" : @"否");
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil);
});
}
}];
}
AVFoundationVideoPlayer-setLooping
/**
* 设置循环播放
*
* @param looping 是否循环播放
* @param error 错误信息输出参数
*
* 功能说明:
* 控制视频是否在播放结束后自动重新开始播放
*/
- (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable*_Nonnull)error {
NSLog(@"🩷 FVPVideoPlayer: 设置循环播放: %@", looping ? @"开启" : @"关闭");
_isLooping = looping;
}
AVFoundationVideoPlayer-setVolume
/**
* 设置播放音量
*
* @param volume 音量值(0.0-1.0)
* @param error 错误信息输出参数
*
* 功能说明:
* 设置播放器的音量,自动限制在有效范围内
*/
- (void)setVolume:(double)volume error:(FlutterError *_Nullable*_Nonnull)error {
float adjustedVolume = (float) ((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume));
NSLog(@"🩷 FVPVideoPlayer: 设置音量: %.2f", adjustedVolume);
_player.volume = adjustedVolume;
}
AVFoundationVideoPlayer-setPlaybackSpeed
/**
* 设置播放速度
*
* @param speed 播放速度倍率
* @param error 错误信息输出参数
*
* 功能说明:
* 设置视频播放的速度倍率,支持快进和慢放
*/
- (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable*_Nonnull)error {
NSLog(@"🩷 FVPVideoPlayer: 设置播放速度: %.2fx", speed);
_targetPlaybackSpeed = @(speed);
[self updatePlayingState];
}
AVFoundationVideoPlayer-duration(特别重要)
- (int64_t)duration {
// Note: https://openradar.appspot.com/radar?id=4968600712511488
// `[AVPlayerItem duration]` can be `kCMTimeIndefinite`,
// use `[[AVPlayerItem asset] duration]` instead.
return FVPCMTimeToMillis([[[_player currentItem] asset] duration]);
}
获取视频总时长。
AVFoundationVideoPlayer-itemDidPlayToEndTime()
/**
* 播放结束通知处理方法
*
* @param notification 播放结束通知对象
*
* 功能说明:
* 当视频播放到结尾时的处理逻辑
* - 如果设置了循环播放,则重新开始播放
* - 如果未设置循环播放,则通知监听器播放完成
*
* 调用时机:
* 接收到AVPlayerItemDidPlayToEndTimeNotification通知时
*/
- (void)itemDidPlayToEndTime:(NSNotification *)notification {
NSLog(@"🩷 FVPVideoPlayer: 收到播放结束通知");
if (_isLooping) {
NSLog(@"🩷 FVPVideoPlayer: 循环播放模式,重新开始播放");
AVPlayerItem *p = [notification object];
[p seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
NSLog(@"🩷 FVPVideoPlayer: 循环播放跳转到开始位置完成,成功: %@",
finished ? @"是" : @"否");
}];
} else {
NSLog(@"🩷 FVPVideoPlayer: 非循环播放模式,通知播放完成");
[self.eventListener videoPlayerDidComplete];
NSLog(@"🩷 FVPVideoPlayer: 播放完成事件已发送");
}
NSLog(@"🩷 FVPVideoPlayer: 播放结束通知处理完成");
}
监听到播放结束的通知。这里来决定是否从头开始循环播放, 还是立即发送播放结束事件给flutter端。
播放完成后发送完成事件。