Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.8.4

* Simplifies native code.

## 2.8.3

* Removes calls to self from init and dealloc, for maintainability.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,13 @@ @interface FVPTextureBasedVideoPlayer ()
// (e.g., after a seek while paused). If YES, the display link should continue to run until the next
// frame is successfully provided.
@property(nonatomic, assign) BOOL waitingForFrame;

/// Ensures that the frame updater runs until a frame is rendered, regardless of play/pause state.
- (void)expectFrame;
@end

@implementation FVPTextureBasedVideoPlayer

- (instancetype)initWithURL:(NSURL *)url
frameUpdater:(FVPFrameUpdater *)frameUpdater
displayLink:(NSObject<FVPDisplayLink> *)displayLink
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider {
NSDictionary<NSString *, id> *options = nil;
if ([headers count] != 0) {
options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
}
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
return [self initWithPlayerItem:item
frameUpdater:frameUpdater
displayLink:displayLink
avFactory:avFactory
viewProvider:viewProvider];
}

- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
frameUpdater:(FVPFrameUpdater *)frameUpdater
displayLink:(NSObject<FVPDisplayLink> *)displayLink
Expand Down Expand Up @@ -81,6 +65,10 @@ - (void)dealloc {

- (void)setTextureIdentifier:(int64_t)textureIdentifier {
self.frameUpdater.textureIdentifier = textureIdentifier;

// Ensure that the first frame is drawn once available, even if the video isn't played, since
// the engine is now expecting the texture to be populated.
[self expectFrame];
}

- (void)expectFrame {
Expand Down Expand Up @@ -118,8 +106,8 @@ - (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable)
}];
}

- (void)dispose {
[super dispose];
- (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error {
[super disposeWithError:error];

[self.playerLayer removeFromSuperlayer];

Expand Down Expand Up @@ -214,7 +202,8 @@ - (CVPixelBufferRef)copyPixelBuffer {
- (void)onTextureUnregistered:(NSObject<FlutterTexture> *)texture {
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.disposed) {
[self dispose];
FlutterError *error;
[self disposeWithError:&error];
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,6 @@ @implementation FVPVideoPlayer {
BOOL _listenersRegistered;
}

- (instancetype)initWithURL:(NSURL *)url
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider {
NSDictionary<NSString *, id> *options = nil;
if ([headers count] != 0) {
options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
}
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
return [self initWithPlayerItem:item avFactory:avFactory viewProvider:viewProvider];
}

- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider {
Expand Down Expand Up @@ -152,7 +139,7 @@ - (void)dealloc {
}
}

- (void)dispose {
- (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error {
// In some hot restart scenarios, dispose can be called twice, so no-op after the first time.
if (_disposed) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
// https://github.com/flutter/packages/pull/6675/#discussion_r1591210702
#import "./include/video_player_avfoundation/messages.g.h"

#if !__has_feature(objc_arc)
#error Code Requires ARC.
#endif

/// Non-test implementation of the diplay link factory.
@interface FVPDefaultDisplayLinkFactory : NSObject <FVPDisplayLinkFactory>
@end
Expand All @@ -48,8 +44,7 @@ @interface FVPVideoPlayerPlugin ()
@property(nonatomic, strong) id<FVPDisplayLinkFactory> displayLinkFactory;
@property(nonatomic, strong) id<FVPAVFactory> avFactory;
@property(nonatomic, strong) NSObject<FVPViewProvider> *viewProvider;
// TODO(stuartmorgan): Decouple identifiers for platform views and texture views.
@property(nonatomic, assign) int64_t nextNonTexturePlayerIdentifier;
@property(nonatomic, assign) int64_t nextPlayerIdentifier;
@end

@implementation FVPVideoPlayerPlugin
Expand Down Expand Up @@ -84,49 +79,39 @@ - (instancetype)initWithAVFactory:(id<FVPAVFactory>)avFactory
_avFactory = avFactory ?: [[FVPDefaultAVFactory alloc] init];
_viewProvider = viewProvider ?: [[FVPDefaultViewProvider alloc] initWithRegistrar:registrar];
_playersByIdentifier = [NSMutableDictionary dictionaryWithCapacity:1];
// Initialized to a high number to avoid collisions with texture identifiers (which are generated
// separately).
_nextNonTexturePlayerIdentifier = INT_MAX;
_nextPlayerIdentifier = 1;
return self;
}

- (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterError *error;
for (FVPVideoPlayer *player in self.playersByIdentifier.allValues) {
// Remove the channel and texture cleanup, and the event listener, to ensure that the player
// doesn't message the engine that is no longer connected.
player.onDisposed = nil;
player.eventListener = nil;
[player dispose];
[player disposeWithError:&error];
}
[self.playersByIdentifier removeAllObjects];
SetUpFVPAVFoundationVideoPlayerApi(registrar.messenger, nil);
}

- (int64_t)onPlayerSetup:(FVPVideoPlayer *)player {
FVPTextureBasedVideoPlayer *textureBasedPlayer =
[player isKindOfClass:[FVPTextureBasedVideoPlayer class]]
? (FVPTextureBasedVideoPlayer *)player
: nil;

int64_t playerIdentifier;
if (textureBasedPlayer) {
playerIdentifier = [self.registrar.textures registerTexture:textureBasedPlayer];
[textureBasedPlayer setTextureIdentifier:playerIdentifier];
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is there a reason for generating playerIdentifier differently for texture based and platform view based players? Looks like it's trying to use texture identifier as player identifier?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historically, there was only the texture ID, and that was used throughout the plugin as the identifier for the player instance. When the platform view variant was added recently, that broke down; we still needed an identifier for each player, but not all players had texture IDs.

The PRs that added platform view support renamed all the platform interface parameters from textureId to playerId since that was simple, but to minimize churn in already-large PRs they didn't disentangle the texture ID from the player ID in the platform implementation internals. This is paying off that tech debt by switching to having a single way to manage player IDs, and reporting texture IDs separately in the texture case.

playerIdentifier = self.nextNonTexturePlayerIdentifier--;
}
- (int64_t)configurePlayer:(FVPVideoPlayer *)player
withExtraDisposeHandler:(nullable void (^)(void))extraDisposeHandler {
int64_t playerIdentifier = self.nextPlayerIdentifier++;
self.playersByIdentifier[@(playerIdentifier)] = player;

NSObject<FlutterBinaryMessenger> *messenger = self.registrar.messenger;
NSString *channelSuffix = [NSString stringWithFormat:@"%lld", playerIdentifier];
// Set up the player-specific API handler, and its onDispose unregistration.
SetUpFVPVideoPlayerInstanceApiWithSuffix(messenger, player, channelSuffix);
__weak typeof(self) weakSelf = self;
BOOL isTextureBased = textureBasedPlayer != nil;
player.onDisposed = ^() {
SetUpFVPVideoPlayerInstanceApiWithSuffix(messenger, nil, channelSuffix);
if (isTextureBased) {
[weakSelf.registrar.textures unregisterTexture:playerIdentifier];
if (extraDisposeHandler) {
extraDisposeHandler();
}
[weakSelf.playersByIdentifier removeObjectForKey:@(playerIdentifier)];
};
// Set up the event channel.
FVPEventBridge *eventBridge = [[FVPEventBridge alloc]
Expand All @@ -135,12 +120,6 @@ - (int64_t)onPlayerSetup:(FVPVideoPlayer *)player {
channelSuffix]];
player.eventListener = eventBridge;

self.playersByIdentifier[@(playerIdentifier)] = player;

// Ensure that the first frame is drawn once available, even if the video isn't played, since
// the engine is now expecting the texture to be populated.
[textureBasedPlayer expectFrame];

return playerIdentifier;
}

Expand Down Expand Up @@ -186,55 +165,64 @@ - (void)initialize:(FlutterError *__autoreleasing *)error {
upgradeAudioSessionCategory(AVAudioSessionCategoryPlayback, 0, 0);
#endif

[self.playersByIdentifier.allValues makeObjectsPerformSelector:@selector(dispose)];
FlutterError *disposeError;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This local variable seems write-only?

Copy link
Collaborator Author

@stuartmorgan-g stuartmorgan-g Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Pigeon generates its methods with non-null error pointers since it should always be calling them with a value, so they don't accidentally get dropped.

The alternative here is that I could make a public dispose and make disposeWithError: call that, and then have these call points call dispose. I couldn't decide at the time if that was worth the extra method, so whichever you prefer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks for the explanation. I'm onboard with keeping the local variable then.

// Disposing a player removes it from the dictionary, so iterate over a copy.
NSArray<FVPVideoPlayer *> *players = [self.playersByIdentifier.allValues copy];
for (FVPVideoPlayer *player in players) {
[player disposeWithError:&disposeError];
}
[self.playersByIdentifier removeAllObjects];
}

- (nullable NSNumber *)createWithOptions:(nonnull FVPCreationOptions *)options
error:(FlutterError **)error {
BOOL textureBased = options.viewType == FVPPlatformVideoViewTypeTextureView;

- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(nonnull FVPCreationOptions *)options
error:(FlutterError **)error {
@try {
FVPVideoPlayer *player = textureBased ? [self texturePlayerWithOptions:options]
: [self platformViewPlayerWithOptions:options];
return @([self onPlayerSetup:player]);
AVPlayerItem *item = [self playerItemWithCreationOptions:options];

// FVPVideoPlayer contains all required logic for platform views.
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:item
avFactory:self.avFactory
viewProvider:self.viewProvider];

return @([self configurePlayer:player withExtraDisposeHandler:nil]);
} @catch (NSException *exception) {
*error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil];
return nil;
}
}

- (nonnull FVPTextureBasedVideoPlayer *)texturePlayerWithOptions:
(nonnull FVPCreationOptions *)options {
FVPFrameUpdater *frameUpdater =
[[FVPFrameUpdater alloc] initWithRegistry:self.registrar.textures];
NSObject<FVPDisplayLink> *displayLink =
[self.displayLinkFactory displayLinkWithRegistrar:_registrar
callback:^() {
[frameUpdater displayLinkFired];
}];

return [[FVPTextureBasedVideoPlayer alloc] initWithURL:[NSURL URLWithString:options.uri]
frameUpdater:frameUpdater
displayLink:displayLink
httpHeaders:options.httpHeaders
avFactory:self.avFactory
viewProvider:self.viewProvider];
}

- (nonnull FVPVideoPlayer *)platformViewPlayerWithOptions:(nonnull FVPCreationOptions *)options {
// FVPVideoPlayer contains all required logic for platform views.
return [[FVPVideoPlayer alloc] initWithURL:[NSURL URLWithString:options.uri]
httpHeaders:options.httpHeaders
avFactory:self.avFactory
viewProvider:self.viewProvider];
}

- (void)disposePlayer:(NSInteger)playerIdentifier error:(FlutterError **)error {
NSNumber *playerKey = @(playerIdentifier);
FVPVideoPlayer *player = self.playersByIdentifier[playerKey];
[self.playersByIdentifier removeObjectForKey:playerKey];
[player dispose];
- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:
(nonnull FVPCreationOptions *)options
error:(FlutterError **)error {
@try {
AVPlayerItem *item = [self playerItemWithCreationOptions:options];
FVPFrameUpdater *frameUpdater =
[[FVPFrameUpdater alloc] initWithRegistry:self.registrar.textures];
NSObject<FVPDisplayLink> *displayLink =
[self.displayLinkFactory displayLinkWithRegistrar:_registrar
callback:^() {
[frameUpdater displayLinkFired];
}];

FVPTextureBasedVideoPlayer *player =
[[FVPTextureBasedVideoPlayer alloc] initWithPlayerItem:item
frameUpdater:frameUpdater
displayLink:displayLink
avFactory:self.avFactory
viewProvider:self.viewProvider];

int64_t textureIdentifier = [self.registrar.textures registerTexture:player];
[player setTextureIdentifier:textureIdentifier];
__weak typeof(self) weakSelf = self;
int64_t playerIdentifier = [self configurePlayer:player
withExtraDisposeHandler:^() {
[weakSelf.registrar.textures unregisterTexture:textureIdentifier];
}];
return [FVPTexturePlayerIds makeWithPlayerId:playerIdentifier textureId:textureIdentifier];
} @catch (NSException *exception) {
*error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil];
return nil;
}
}

- (void)setMixWithOthers:(BOOL)mixWithOthers
Expand Down Expand Up @@ -274,4 +262,14 @@ - (nullable NSString *)fileURLForAssetWithName:(NSString *)asset
return [NSURL fileURLWithPath:path].absoluteString;
}

/// Returns the AVPlayerItem corresponding to the given player creation options.
- (nonnull AVPlayerItem *)playerItemWithCreationOptions:(nonnull FVPCreationOptions *)options {
NSDictionary<NSString *, NSString *> *headers = options.httpHeaders;
NSDictionary<NSString *, id> *itemOptions =
headers.count == 0 ? nil : @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:options.uri]
options:itemOptions];
return [AVPlayerItem playerItemWithAsset:asset];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,17 @@ NS_ASSUME_NONNULL_BEGIN
/// updates frames, and handles display link callbacks.
/// If you need to display a video using platform view, use FVPVideoPlayer instead.
@interface FVPTextureBasedVideoPlayer : FVPVideoPlayer <FlutterTexture>
/// Initializes a new instance of FVPTextureBasedVideoPlayer with the given URL, frame updater,
/// display link, HTTP headers, AV factory, and registrar.
- (instancetype)initWithURL:(NSURL *)url
frameUpdater:(FVPFrameUpdater *)frameUpdater
displayLink:(NSObject<FVPDisplayLink> *)displayLink
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider;
/// Initializes a new instance of FVPTextureBasedVideoPlayer with the given player item,
/// frame updater, display link, AV factory, and view provider.
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
frameUpdater:(FVPFrameUpdater *)frameUpdater
displayLink:(NSObject<FVPDisplayLink> *)displayLink
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider;

/// Sets the texture Identifier for the frame updater. This method should be called once the texture
/// identifier is obtained from the texture registry.
- (void)setTextureIdentifier:(int64_t)textureIdentifier;

/// Tells the player to run its frame updater until it receives a frame, regardless of the
/// play/pause state.
- (void)expectFrame;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,11 @@ NS_ASSUME_NONNULL_BEGIN
/// A block that will be called when dispose is called.
@property(nonatomic, nullable, copy) void (^onDisposed)(void);

/// Initializes a new instance of FVPVideoPlayer with the given URL, HTTP headers, AV factory, and
/// view provider.
- (instancetype)initWithURL:(NSURL *)url
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider;

/// Disposes the video player and releases any resources it holds.
- (void)dispose;
/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, AV factory, and view
/// provider.
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider;

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ NS_ASSUME_NONNULL_BEGIN
/// Indicates whether the video player has been initialized.
@property(nonatomic, readonly) BOOL isInitialized;

/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, frame updater, display
/// link, AV factory, and view provider.
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
avFactory:(id<FVPAVFactory>)avFactory
viewProvider:(NSObject<FVPViewProvider> *)viewProvider;

/// Updates the playing state of the video player.
- (void)updatePlayingState;
@end
Expand Down
Loading