flutter视频播放器video_player_avfoundation之FVPVideoPlayer、AVFoundationVideoPlayer(二)

178 阅读14分钟
/**
 * 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],
    };
}

截屏2025-09-25 17.11.12.png

所有观察者注册完成后, 观察者的回调方法就会不断接收到监听属性的回调。开发者在监听到的回调里处理逻辑。

(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);
    }

}

截屏2025-09-28 11.18.27.png

KVO首先监听到的是视频时长duration和尺寸presentationSize的变化。

当前播放项AVPlayerItem的状态是AVPlayerItemStatusUnknownAVPlayerItemStatusFailed时, 控制台只打印日志,标记KVO回调处理完成。

当前播放项AVPlayerItem已经准备就绪状态AVPlayerItemStatusReadyToPlay下, 立即调用reportInitializedIfReadyToPlay 报告初始化完成。

loadedTimeRanges

紧接着KVO持续接收到loadedTimeRanges缓冲时间范围变化的通知回调。原生端在这个回调里,将缓冲时间范围持续发送给flutter,通过调用EventBridge里的videoPlayerDidUpdateBufferRegions方法。

视频缓冲区更新事件:

[self sendOrQueue:@{@"event": @"bufferingUpdate", @"values": regions}];

截屏2025-09-28 12.47.11.png

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端发送开始缓冲的消息,videoPlayerDidStartBufferingvideoPlayerDidEndBuffering消息会持续发送。

截屏2025-09-28 14.04.35.png

截屏2025-09-28 14.04.58.png

截屏2025-09-28 14.05.16.png

截屏2025-09-28 14.05.23.png

截屏2025-09-28 14.05.35.png

从日志可以看到, 缓冲不足缓冲充足的回调会持续执行。

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字段停止掉,不继续执行真正更新播放状态的代码逻辑。

默认播放状态_isPlayingfalse, 也就是未播放状态_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: 初始化检查完成");
}

截屏2025-09-28 15.06.04.png

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];
} 

截屏2025-09-28 15.10.42.png

缓冲阶段, 默认播放速率是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发送开始播放事件。

截屏2025-09-28 15.27.40.png

当用户在flutter端手动暂停时, 播放速率的KVO回调会监听到rate=0,就会发送暂停播放事件。

AVFoundationVideoPlayer-pauseWithError

/**
 * 暂停播放视频
 * 
 * @param error 错误信息输出参数
 * 
 * 功能说明:
 * 设置播放状态为NO,并更新播放器的实际播放状态
 */
- (void)pauseWithError:(FlutterError *_Nullable*_Nonnull)error {
    NSLog(@"🩷 FVPVideoPlayer: 暂停播放视频");
    _isPlaying = NO;
    [self updatePlayingState];
    NSLog(@"🩷 FVPVideoPlayer: 暂停状态已更新");
}

截屏2025-09-28 15.32.57.png

可以看到, 发送给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端。

播放完成后发送完成事件

截屏2025-09-28 15.41.38.png