From 7b927ebe5e52a7a4651246235f925b915cdd91e7 Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Tue, 27 Jan 2026 19:52:07 +0200 Subject: [PATCH 1/9] [video_player] Add background playback with media notification support Adds the ability for video playback to continue when the app is backgrounded, with optional system media notification controls on Android and iOS/macOS. Features: - New `allowBackgroundPlayback` option in `VideoPlayerOptions` - New `NotificationMetadata` class for customizing media notification - System media controls (play/pause/seek) on lock screen and control center - `updateNotificationMetadata()` method for dynamic metadata updates Platform support: - Android: Uses Media3 MediaSession with foreground service - iOS/macOS: Uses MPNowPlayingInfoCenter and MPRemoteCommandCenter - Web: Not supported (returns false for availability check) --- .../video_player/video_player/CHANGELOG.md | 1 + .../video_player/example/pubspec.yaml | 7 + .../video_player/lib/video_player.dart | 43 ++ .../video_player/video_player/pubspec.yaml | 7 + .../video_player/test/video_player_test.dart | 27 + .../video_player_android/CHANGELOG.md | 4 + .../video_player_android/android/build.gradle | 1 + .../android/src/main/AndroidManifest.xml | 15 + .../VideoMedia3SessionService.java | 591 ++++++++++++++++++ .../plugins/videoplayer/VideoPlayer.java | 145 +++++ .../videoplayer/VideoPlayerPlugin.java | 3 + .../flutter/plugins/videoplayer/Messages.kt | 160 ++++- .../video_player_android/example/pubspec.yaml | 4 + .../lib/src/android_video_player.dart | 57 ++ .../lib/src/messages.g.dart | 552 ++++++++++------ .../pigeons/messages.dart | 36 ++ .../video_player_android/pubspec.yaml | 6 +- .../test/android_video_player_test.dart | 127 ++++ .../test/android_video_player_test.mocks.dart | 21 + .../video_player_avfoundation/CHANGELOG.md | 4 + .../FVPVideoPlayer.m | 362 +++++++++++ .../video_player_avfoundation/messages.g.h | 102 +-- .../video_player_avfoundation/messages.g.m | 419 +++++++------ .../example/pubspec.yaml | 4 + .../lib/src/avfoundation_video_player.dart | 55 ++ .../lib/src/messages.g.dart | 168 +++++ .../pigeons/messages.dart | 34 + .../video_player_avfoundation/pubspec.yaml | 6 +- .../test/avfoundation_video_player_test.dart | 129 ++++ .../avfoundation_video_player_test.mocks.dart | 45 ++ .../CHANGELOG.md | 3 +- .../lib/video_player_platform_interface.dart | 124 ++++ .../pubspec.yaml | 2 +- .../test/video_player_options_test.dart | 25 + .../video_player_platform_interface_test.dart | 106 ++++ .../video_player_web/CHANGELOG.md | 3 +- .../video_player_web/example/pubspec.yaml | 4 + .../lib/video_player_web.dart | 19 + .../video_player_web/pubspec.yaml | 6 +- 39 files changed, 3006 insertions(+), 421 deletions(-) create mode 100644 packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 3f70c390c861..67b4d95e25c4 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,6 @@ ## 2.11.0 +* Adds background playback with system media notification support. * Adds `getAudioTracks()` and `selectAudioTrack()` methods to retrieve and select available audio tracks. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. * Updates README to reflect currently supported OS versions for the latest diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 09dc13ba5baa..cdeca06de5b0 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -35,3 +35,10 @@ flutter: - assets/bumble_bee_captions.srt - assets/bumble_bee_captions.vtt - assets/Audio.mp3 +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_android: {path: ../../video_player_android} + video_player_avfoundation: {path: ../../video_player_avfoundation} + video_player_platform_interface: {path: ../../video_player_platform_interface} + video_player_web: {path: ../../video_player_web} diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 232dda0858b7..c4c37e626a78 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -18,6 +18,7 @@ export 'package:video_player_platform_interface/video_player_platform_interface. show DataSourceType, DurationRange, + NotificationMetadata, VideoFormat, VideoPlayerOptions, VideoPlayerWebOptions, @@ -585,6 +586,19 @@ class VideoPlayerController extends ValueNotifier { (await _videoPlayerPlatform.createWithOptions(creationOptions)) ?? kUninitializedPlayerId; _creatingCompleter!.complete(null); + + // Set up background playback if enabled + final platform_interface.VideoPlayerOptions? options = videoPlayerOptions; + final platform_interface.NotificationMetadata? metadata = + options?.notificationMetadata; + if (options != null && options.allowBackgroundPlayback) { + await _videoPlayerPlatform.setBackgroundPlayback( + _playerId, + enableBackground: true, + notificationMetadata: metadata, + ); + } + final initializingCompleter = Completer(); // Apply the web-specific options @@ -847,6 +861,35 @@ class VideoPlayerController extends ValueNotifier { await _applyPlaybackSpeed(); } + /// Updates the notification metadata. + /// + /// This updates the metadata shown in the system notification, lock screen, + /// and control center. Only works when background playback is enabled with + /// notification metadata. + /// + /// The [notificationMetadata] should contain the updated title, artist, album, + /// artwork URI, etc. + Future updateNotificationMetadata( + platform_interface.NotificationMetadata notificationMetadata, + ) async { + if (_isDisposedOrNotInitialized) { + return; + } + + if (videoPlayerOptions?.notificationMetadata == null) { + throw StateError( + 'Cannot update notification metadata when notificationMetadata was not ' + 'provided in VideoPlayerOptions. Provide notificationMetadata to enable ' + 'notifications.', + ); + } + + await _videoPlayerPlatform.updateNotificationMetadata( + _playerId, + notificationMetadata, + ); + } + /// Sets the caption offset. /// /// The [offset] will be used when getting the correct caption for a specific position. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index f48636e14cde..e308d168bd8b 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -38,3 +38,10 @@ dev_dependencies: topics: - video - video-player +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_android: {path: ../video_player_android} + video_player_avfoundation: {path: ../video_player_avfoundation} + video_player_platform_interface: {path: ../video_player_platform_interface} + video_player_web: {path: ../video_player_web} diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 70bbf643675d..45187c708e7e 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -131,6 +131,11 @@ class FakeController extends ValueNotifier } String? selectedAudioTrackId; + + @override + Future updateNotificationMetadata( + NotificationMetadata notificationMetadata, + ) async {} } Future _loadClosedCaption() async => @@ -1904,4 +1909,26 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } final Map selectedAudioTrackIds = {}; + + @override + bool isBackgroundPlaybackSupportAvailable() { + return true; + } + + @override + Future setBackgroundPlayback( + int playerId, { + required bool enableBackground, + NotificationMetadata? notificationMetadata, + }) async { + calls.add('setBackgroundPlayback'); + } + + @override + Future updateNotificationMetadata( + int playerId, + NotificationMetadata notificationMetadata, + ) async { + calls.add('updateNotificationMetadata'); + } } diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index cf70c13aa770..38dffed747a6 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.10.0 + +* Adds background playback with system media notification support. + ## 2.9.3 * Updates `androidx.media3` to 1.9.1. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 714bb410f871..cf1195815d30 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -57,6 +57,7 @@ android { implementation("androidx.media3:media3-exoplayer-dash:${exoplayer_version}") implementation("androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}") implementation("androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}") + implementation("androidx.media3:media3-session:${exoplayer_version}") testImplementation("junit:junit:4.13.2") testImplementation("androidx.test:core:1.7.0") testImplementation("org.mockito:mockito-core:5.21.0") diff --git a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml index e6e98bfe5a31..753ea38fce59 100644 --- a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml @@ -1,3 +1,18 @@ + + + + + + + + + + + + diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java new file mode 100644 index 000000000000..28ad1caca8cc --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java @@ -0,0 +1,591 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.session.CommandButton; +import androidx.media3.session.DefaultMediaNotificationProvider; +import androidx.media3.session.MediaSession; +import androidx.media3.session.MediaSessionService; +import androidx.media3.session.SessionCommand; +import androidx.media3.session.SessionResult; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Media3 MediaSessionService for background video playback. Implements the modern Media3 + * MediaSession API for automatic notification management and better system integration. + * + *

Based on: https://developer.android.com/media/media3/session/background-playback + */ +public class VideoMedia3SessionService extends MediaSessionService { + private static final String TAG = "VideoMedia3SessionService"; + private static final String CHANNEL_ID = "video_player_channel"; + + // Map of texture IDs to media sessions + private final Map mediaSessions = new HashMap<>(); + + // Track the primary session for the service + @Nullable private MediaSession primarySession = null; + + // Executor for async operations like artwork loading + private final ExecutorService executorService = Executors.newCachedThreadPool(); + + // Handler for main thread operations + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + // Binder for local service connection + private final android.os.IBinder binder = new LocalBinder(); + + /** Local binder for binding to this service from VideoPlayerPlugin. */ + public class LocalBinder extends android.os.Binder { + public VideoMedia3SessionService getService() { + return VideoMedia3SessionService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + + // Create notification channel FIRST (required for Android O+) + createNotificationChannel(); + + // Configure Media3 to automatically handle notifications + setMediaNotificationProvider( + new DefaultMediaNotificationProvider.Builder(this).setChannelId(CHANNEL_ID).build()); + + Log.d(TAG, "VideoMedia3SessionService created with notification provider"); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = + new NotificationChannel(CHANNEL_ID, "Video Playback", NotificationManager.IMPORTANCE_LOW); + channel.setDescription("Media playback controls"); + channel.setShowBadge(false); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + Log.d(TAG, "Notification channel created: " + CHANNEL_ID); + } + } + } + + @Override + @Nullable + public android.os.IBinder onBind(@NonNull Intent intent) { + // If this is a MediaSession connection, use the superclass binding + if (intent.getAction() != null + && intent.getAction().equals(MediaSessionService.SERVICE_INTERFACE)) { + return super.onBind(intent); + } + // Otherwise, return our local binder for plugin access + return binder; + } + + /** + * Called when a controller (like MediaController or notification) requests the session. Returns + * the primary MediaSession or null if none exists. + */ + @Override + @Nullable + public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) { + Log.d(TAG, "onGetSession called for controller: " + controllerInfo.getPackageName()); + return primarySession; + } + + /** + * Creates a MediaSession for a video player. This is the main entry point for enabling background + * playback. + * + * @param textureId Unique identifier for the video player + * @param player The ExoPlayer instance + * @param notificationMetadata Metadata about the media being played + * @return The created MediaSession, or null if creation fails + */ + @Nullable + public MediaSession createMediaSession( + int textureId, + @NonNull ExoPlayer player, + @Nullable NotificationMetadataMessage notificationMetadata) { + + try { + Log.d(TAG, "Creating Media3 MediaSession for texture: " + textureId); + + // Check if session already exists for this texture + if (mediaSessions.containsKey(textureId)) { + Log.w(TAG, "MediaSession already exists for texture: " + textureId); + return mediaSessions.get(textureId); + } + + // Validate player + if (player == null) { + Log.e(TAG, "Cannot create MediaSession with null player"); + return null; + } + + // Update player metadata if provided + if (notificationMetadata != null) { + updatePlayerMetadata(player, notificationMetadata); + } + + // Create session activity (opens the app when notification is tapped) + Intent openAppIntent = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (openAppIntent != null) { + openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + PendingIntent sessionActivity = + PendingIntent.getActivity( + this, + textureId, + openAppIntent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + + // Build MediaSession with callback + MediaSession session = + new MediaSession.Builder(this, player) + .setId("video_player_" + textureId) + .setSessionActivity(sessionActivity) + .setCallback(new MediaSessionCallback()) + .build(); + + // Store the session + mediaSessions.put(textureId, session); + + // Set as primary session if first one + if (primarySession == null) { + primarySession = session; + Log.d(TAG, "Set primary MediaSession for texture: " + textureId); + } + + // CRITICAL: Register the session with Media3's notification system + // This tells the MediaSessionService about the active session + try { + addSession(session); + Log.d(TAG, "Called addSession() to register with Media3 notification system"); + } catch (Exception e) { + Log.e(TAG, "Error calling addSession()", e); + } + + // Add listener to log playback state changes for debugging + player.addListener( + new Player.Listener() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + Log.d(TAG, "Player isPlaying changed: " + isPlaying); + } + + @Override + public void onPlaybackStateChanged(int playbackState) { + String stateStr = + playbackState == Player.STATE_IDLE + ? "IDLE" + : playbackState == Player.STATE_BUFFERING + ? "BUFFERING" + : playbackState == Player.STATE_READY + ? "READY" + : playbackState == Player.STATE_ENDED ? "ENDED" : "UNKNOWN"; + Log.d( + TAG, + "Player playbackState changed: " + + stateStr + + " (isPlaying=" + + player.isPlaying() + + ")"); + } + }); + + Log.d( + TAG, + "MediaSession created successfully for texture: " + + textureId + + ", player.isPlaying=" + + player.isPlaying() + + ", playbackState=" + + player.getPlaybackState()); + return session; + + } catch (Exception e) { + Log.e(TAG, "Error creating MediaSession for texture: " + textureId, e); + return null; + } + } + + /** + * Updates the metadata on an existing MediaSession. + * + * @param textureId The texture ID of the player + * @param notificationMetadata The new metadata + */ + public void updateMediaSessionMetadata( + int textureId, @NonNull NotificationMetadataMessage notificationMetadata) { + + MediaSession session = mediaSessions.get(textureId); + if (session == null) { + Log.w(TAG, "No MediaSession found for texture: " + textureId); + return; + } + + Player player = session.getPlayer(); + if (player instanceof ExoPlayer) { + updatePlayerMetadata((ExoPlayer) player, notificationMetadata); + } + } + + /** + * Updates the ExoPlayer's current MediaItem with new metadata. This will automatically update the + * notification. + */ + private void updatePlayerMetadata( + @NonNull ExoPlayer player, @NonNull NotificationMetadataMessage metadata) { + + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); + + if (metadata.getTitle() != null) { + metadataBuilder.setTitle(metadata.getTitle()); + metadataBuilder.setDisplayTitle(metadata.getTitle()); + } + if (metadata.getArtist() != null) { + metadataBuilder.setArtist(metadata.getArtist()); + } + if (metadata.getAlbum() != null) { + metadataBuilder.setAlbumTitle(metadata.getAlbum()); + } + + // Load artwork asynchronously if provided + if (metadata.getArtUri() != null) { + loadArtworkAsync( + metadata.getArtUri(), + bitmap -> { + if (bitmap != null) { + mainHandler.post( + () -> { + try { + MediaMetadata currentMetadata = player.getMediaMetadata(); + MediaMetadata.Builder updatedBuilder = currentMetadata.buildUpon(); + + byte[] artworkData = bitmapToByteArray(bitmap); + updatedBuilder.setArtworkData( + artworkData, MediaMetadata.PICTURE_TYPE_FRONT_COVER); + + // Update the current media item with artwork + MediaItem currentItem = player.getCurrentMediaItem(); + if (currentItem != null) { + MediaItem updatedItem = + currentItem + .buildUpon() + .setMediaMetadata(updatedBuilder.build()) + .build(); + // Use replaceMediaItem to avoid disrupting playback + player.replaceMediaItem(0, updatedItem); + Log.d(TAG, "Artwork loaded and updated"); + } + } catch (Exception e) { + Log.e(TAG, "Error updating artwork", e); + } + }); + } + }); + } + + // Update the current media item with metadata (without artwork initially) + MediaItem currentItem = player.getCurrentMediaItem(); + if (currentItem != null) { + MediaItem updatedItem = + currentItem.buildUpon().setMediaMetadata(metadataBuilder.build()).build(); + // Use replaceMediaItem to avoid disrupting playback + player.replaceMediaItem(0, updatedItem); + Log.d(TAG, "Player metadata updated"); + } else { + Log.w(TAG, "Player has no current MediaItem to update with metadata"); + } + } + + /** Callback interface for async artwork loading. */ + private interface ArtworkCallback { + void onArtworkLoaded(@Nullable Bitmap bitmap); + } + + /** Asynchronously loads artwork from a URI. */ + private void loadArtworkAsync(@NonNull String artUri, @NonNull ArtworkCallback callback) { + executorService.execute( + () -> { + Bitmap bitmap = null; + try { + Uri uri = Uri.parse(artUri); + String scheme = uri.getScheme(); + + if ("http".equals(scheme) || "https".equals(scheme)) { + bitmap = loadArtworkFromNetwork(artUri); + } else if ("file".equals(scheme) || "content".equals(scheme)) { + bitmap = loadArtworkFromContentUri(uri); + } + + if (bitmap != null) { + bitmap = scaleBitmap(bitmap, 512); // Max 512x512 for notification + } + } catch (Exception e) { + Log.e(TAG, "Error loading artwork", e); + } + + final Bitmap finalBitmap = bitmap; + mainHandler.post(() -> callback.onArtworkLoaded(finalBitmap)); + }); + } + + private Bitmap loadArtworkFromNetwork(String urlString) throws Exception { + HttpURLConnection connection = null; + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.connect(); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + InputStream input = connection.getInputStream(); + return BitmapFactory.decodeStream(input); + } + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private Bitmap loadArtworkFromContentUri(Uri uri) throws Exception { + InputStream input = getContentResolver().openInputStream(uri); + if (input != null) { + try { + return BitmapFactory.decodeStream(input); + } finally { + input.close(); + } + } + return null; + } + + private Bitmap scaleBitmap(Bitmap bitmap, int maxSize) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + if (width <= maxSize && height <= maxSize) { + return bitmap; + } + + float scale = Math.min((float) maxSize / width, (float) maxSize / height); + int newWidth = Math.round(width * scale); + int newHeight = Math.round(height * scale); + + Bitmap scaled = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); + if (scaled != bitmap) { + bitmap.recycle(); + } + return scaled; + } + + private byte[] bitmapToByteArray(Bitmap bitmap) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + return stream.toByteArray(); + } + + /** Removes a media session when player is disposed. */ + public void removeMediaSession(int textureId) { + try { + MediaSession session = mediaSessions.remove(textureId); + + if (session != null) { + Log.d(TAG, "Removing MediaSession for texture: " + textureId); + + if (session == primarySession) { + primarySession = null; + + // If there are other sessions, promote one to primary + if (!mediaSessions.isEmpty()) { + primarySession = mediaSessions.values().iterator().next(); + Log.d(TAG, "Promoted new primary MediaSession"); + } + } + + // Unregister from Media3 notification system + try { + removeSession(session); + } catch (Exception e) { + Log.e(TAG, "Error calling removeSession", e); + } + + // Release the session + try { + session.release(); + } catch (Exception e) { + Log.e(TAG, "Error releasing MediaSession", e); + } + } else { + Log.w(TAG, "No MediaSession found for texture: " + textureId); + } + + // Stop service if no more sessions + if (mediaSessions.isEmpty()) { + Log.d(TAG, "No more sessions, stopping service"); + stopSelf(); + } else { + Log.d(TAG, "Active sessions remaining: " + mediaSessions.size()); + } + + } catch (Exception e) { + Log.e(TAG, "Error in removeMediaSession for texture: " + textureId, e); + } + } + + /** MediaSession.Callback to handle playback commands and controller connections. */ + private class MediaSessionCallback implements MediaSession.Callback { + + @Override + public MediaSession.ConnectionResult onConnect( + @NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) { + + Log.d(TAG, "Controller connecting: " + controller.getPackageName()); + + // Configure available commands for the controller + MediaSession.ConnectionResult.AcceptedResultBuilder resultBuilder = + new MediaSession.ConnectionResult.AcceptedResultBuilder(session); + + // For media notification controller, configure button preferences + if (session.isMediaNotificationController(controller)) { + Log.d(TAG, "Configuring media notification controller"); + + // Set available player commands + Player.Commands availableCommands = + new Player.Commands.Builder() + .addAll( + Player.COMMAND_PLAY_PAUSE, + Player.COMMAND_PREPARE, + Player.COMMAND_STOP, + Player.COMMAND_SEEK_TO_DEFAULT_POSITION, + Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, + Player.COMMAND_SEEK_BACK, + Player.COMMAND_SEEK_FORWARD, + Player.COMMAND_SET_SPEED_AND_PITCH, + Player.COMMAND_GET_CURRENT_MEDIA_ITEM, + Player.COMMAND_GET_TIMELINE, + Player.COMMAND_GET_METADATA) + .build(); + + resultBuilder.setAvailablePlayerCommands(availableCommands); + + // Configure media button layout for notification + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_SEEK_BACK) + .setDisplayName("Rewind") + .build(), + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setDisplayName("Play/Pause") + .build(), + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_SEEK_FORWARD) + .setDisplayName("Fast Forward") + .build()); + + resultBuilder.setMediaButtonPreferences(customLayout); + } + + return resultBuilder.build(); + } + + @Override + public ListenableFuture onCustomCommand( + @NonNull MediaSession session, + @NonNull MediaSession.ControllerInfo controller, + @NonNull SessionCommand customCommand, + @NonNull Bundle args) { + + Log.d(TAG, "Custom command received: " + customCommand.customAction); + return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); + } + + @Override + public void onDisconnected( + @NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) { + Log.d(TAG, "Controller disconnected: " + controller.getPackageName()); + } + } + + @Override + public void onTaskRemoved(@Nullable Intent rootIntent) { + Log.d(TAG, "Task removed - app swiped away"); + + // Stop all playback when app is removed from recents + for (MediaSession session : mediaSessions.values()) { + Player player = session.getPlayer(); + if (player != null && player.isPlaying()) { + player.pause(); + } + } + + // Stop the service + stopSelf(); + + super.onTaskRemoved(rootIntent); + } + + @Override + public void onDestroy() { + Log.d(TAG, "VideoMedia3SessionService destroyed"); + + try { + // Release all sessions + for (MediaSession session : mediaSessions.values()) { + try { + session.release(); + } catch (Exception e) { + Log.e(TAG, "Error releasing session in onDestroy", e); + } + } + mediaSessions.clear(); + primarySession = null; + + executorService.shutdown(); + } catch (Exception e) { + Log.e(TAG, "Error in onDestroy", e); + } + + super.onDestroy(); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 7cfb5c1c13be..e32cc2ff36fe 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -7,6 +7,12 @@ import static androidx.media3.common.Player.REPEAT_MODE_ALL; import static androidx.media3.common.Player.REPEAT_MODE_OFF; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; @@ -20,6 +26,7 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.session.MediaSession; import io.flutter.view.TextureRegistry.SurfaceProducer; import java.util.ArrayList; import java.util.List; @@ -30,6 +37,8 @@ *

It provides methods to control playback, adjust volume, and handle seeking. */ public abstract class VideoPlayer implements VideoPlayerInstanceApi { + private static final String TAG = "VideoPlayer"; + @NonNull protected final VideoPlayerCallbacks videoPlayerEvents; @Nullable protected final SurfaceProducer surfaceProducer; @Nullable private DisposeHandler disposeHandler; @@ -37,6 +46,41 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @Nullable protected DefaultTrackSelector trackSelector; + // Background playback support + @Nullable protected Context applicationContext; + @Nullable protected VideoMedia3SessionService mediaSessionService; + @Nullable protected MediaSession mediaSession; + protected boolean backgroundPlaybackEnabled = false; + protected int textureId = -1; + private boolean serviceBound = false; + + private final ServiceConnection serviceConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + VideoMedia3SessionService.LocalBinder binder = + (VideoMedia3SessionService.LocalBinder) service; + mediaSessionService = binder.getService(); + serviceBound = true; + Log.d(TAG, "MediaSessionService connected"); + + // If background playback was requested before service was connected, set it up now + if (backgroundPlaybackEnabled && pendingNotificationMetadata != null) { + createMediaSessionInternal(pendingNotificationMetadata); + pendingNotificationMetadata = null; + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaSessionService = null; + serviceBound = false; + Log.d(TAG, "MediaSessionService disconnected"); + } + }; + + @Nullable private NotificationMetadataMessage pendingNotificationMetadata; + /** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */ public interface ExoPlayerProvider { /** @@ -233,10 +277,111 @@ public void selectAudioTrack(long groupIndex, long trackIndex) { trackSelector.buildUponParameters().setOverrideForType(override).build()); } + /** + * Sets the application context and texture ID for background playback support. This should be + * called by the plugin when creating a player. + */ + public void setBackgroundPlaybackContext(@NonNull Context context, int textureId) { + this.applicationContext = context.getApplicationContext(); + this.textureId = textureId; + } + + @Override + public void setBackgroundPlayback(@NonNull BackgroundPlaybackMessage msg) { + backgroundPlaybackEnabled = msg.getEnableBackground(); + + if (!backgroundPlaybackEnabled) { + // Disable background playback - remove media session + removeMediaSession(); + return; + } + + NotificationMetadataMessage metadata = msg.getNotificationMetadata(); + if (metadata == null) { + // Background playback without notification - just keep the flag + Log.d(TAG, "Background playback enabled without notification metadata"); + return; + } + + // Check if service is already connected + if (mediaSessionService != null && serviceBound) { + createMediaSessionInternal(metadata); + } else { + // Store metadata and bind to service + pendingNotificationMetadata = metadata; + bindMediaSessionService(); + } + } + + @Override + public void updateNotificationMetadata(@NonNull NotificationMetadataMessage msg) { + if (mediaSessionService != null && textureId >= 0) { + mediaSessionService.updateMediaSessionMetadata(textureId, msg); + } else { + Log.w(TAG, "Cannot update notification metadata: service not available"); + } + } + + private void bindMediaSessionService() { + if (applicationContext == null) { + Log.e(TAG, "Cannot bind to MediaSessionService: no application context"); + return; + } + + if (serviceBound) { + return; + } + + Intent intent = new Intent(applicationContext, VideoMedia3SessionService.class); + try { + applicationContext.startService(intent); + applicationContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + Log.d(TAG, "Binding to MediaSessionService"); + } catch (Exception e) { + Log.e(TAG, "Failed to bind to MediaSessionService", e); + } + } + + private void createMediaSessionInternal(@NonNull NotificationMetadataMessage metadata) { + if (mediaSessionService == null || textureId < 0) { + Log.w(TAG, "Cannot create media session: service not available or invalid texture ID"); + return; + } + + mediaSession = mediaSessionService.createMediaSession(textureId, exoPlayer, metadata); + if (mediaSession != null) { + Log.d(TAG, "Media session created for texture: " + textureId); + } + } + + private void removeMediaSession() { + if (mediaSessionService != null && textureId >= 0) { + mediaSessionService.removeMediaSession(textureId); + mediaSession = null; + } + } + + private void unbindMediaSessionService() { + if (applicationContext != null && serviceBound) { + try { + applicationContext.unbindService(serviceConnection); + serviceBound = false; + Log.d(TAG, "Unbound from MediaSessionService"); + } catch (Exception e) { + Log.e(TAG, "Error unbinding from MediaSessionService", e); + } + } + } + public void dispose() { if (disposeHandler != null) { disposeHandler.onDispose(); } + + // Clean up background playback resources + removeMediaSession(); + unbindMediaSessionService(); + exoPlayer.release(); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 49adaf4b7b33..880a518b4169 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -146,6 +146,9 @@ public long createForPlatformView(@NonNull CreationOptions options) { } private void registerPlayerInstance(VideoPlayer player, long id) { + // Set up background playback context + player.setBackgroundPlaybackContext(flutterState.applicationContext, (int) id); + // Set up the instance-specific API handler, and make sure it is removed when the player is // disposed. BinaryMessenger messenger = flutterState.binaryMessenger; diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index e546c744e561..13dae5c5da80 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.7), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -557,6 +557,100 @@ data class NativeAudioTrackData( override fun hashCode(): Int = toList().hashCode() } +/** + * Metadata for the system media notification when playing in background. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class NotificationMetadataMessage( + val id: String, + val title: String? = null, + val album: String? = null, + val artist: String? = null, + val durationMs: Long? = null, + val artUri: String? = null, + /** + * The name of a drawable resource to use as the notification's small icon. Android only. If + * null, defaults to Media3's default icon. + */ + val smallIconResourceName: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): NotificationMetadataMessage { + val id = pigeonVar_list[0] as String + val title = pigeonVar_list[1] as String? + val album = pigeonVar_list[2] as String? + val artist = pigeonVar_list[3] as String? + val durationMs = pigeonVar_list[4] as Long? + val artUri = pigeonVar_list[5] as String? + val smallIconResourceName = pigeonVar_list[6] as String? + return NotificationMetadataMessage( + id, title, album, artist, durationMs, artUri, smallIconResourceName) + } + } + + fun toList(): List { + return listOf( + id, + title, + album, + artist, + durationMs, + artUri, + smallIconResourceName, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is NotificationMetadataMessage) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Message for configuring background playback with media notification. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class BackgroundPlaybackMessage( + val enableBackground: Boolean, + val notificationMetadata: NotificationMetadataMessage? = null +) { + companion object { + fun fromList(pigeonVar_list: List): BackgroundPlaybackMessage { + val enableBackground = pigeonVar_list[0] as Boolean + val notificationMetadata = pigeonVar_list[1] as NotificationMetadataMessage? + return BackgroundPlaybackMessage(enableBackground, notificationMetadata) + } + } + + fun toList(): List { + return listOf( + enableBackground, + notificationMetadata, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is BackgroundPlaybackMessage) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -601,6 +695,12 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { 141.toByte() -> { return (readValue(buffer) as? List)?.let { NativeAudioTrackData.fromList(it) } } + 142.toByte() -> { + return (readValue(buffer) as? List)?.let { NotificationMetadataMessage.fromList(it) } + } + 143.toByte() -> { + return (readValue(buffer) as? List)?.let { BackgroundPlaybackMessage.fromList(it) } + } else -> super.readValueOfType(type, buffer) } } @@ -659,6 +759,14 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(141) writeValue(stream, value.toList()) } + is NotificationMetadataMessage -> { + stream.write(142) + writeValue(stream, value.toList()) + } + is BackgroundPlaybackMessage -> { + stream.write(143) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -854,6 +962,10 @@ interface VideoPlayerInstanceApi { fun getAudioTracks(): NativeAudioTrackData /** Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] */ fun selectAudioTrack(groupIndex: Long, trackIndex: Long) + /** Configures background playback and media notification. */ + fun setBackgroundPlayback(msg: BackgroundPlaybackMessage) + /** Updates the notification metadata. */ + fun updateNotificationMetadata(msg: NotificationMetadataMessage) companion object { /** The codec used by VideoPlayerInstanceApi. */ @@ -1088,6 +1200,52 @@ interface VideoPlayerInstanceApi { channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setBackgroundPlayback$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val msgArg = args[0] as BackgroundPlaybackMessage + val wrapped: List = + try { + api.setBackgroundPlayback(msgArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.updateNotificationMetadata$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val msgArg = args[0] as NotificationMetadataMessage + val wrapped: List = + try { + api.updateNotificationMetadata(msgArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 07c5b497d5d2..24e2db4d9774 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -34,3 +34,7 @@ flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../../video_player_platform_interface} diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 84249bd41afd..c505cf4354e9 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -266,6 +266,55 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return true; } + @override + bool isBackgroundPlaybackSupportAvailable() { + // Android supports background playback with media notifications + return true; + } + + @override + Future setBackgroundPlayback( + int playerId, { + required bool enableBackground, + NotificationMetadata? notificationMetadata, + }) { + NotificationMetadataMessage? metadataMessage; + if (notificationMetadata != null) { + metadataMessage = NotificationMetadataMessage( + id: notificationMetadata.id, + title: notificationMetadata.title, + artist: notificationMetadata.artist, + album: notificationMetadata.album, + durationMs: notificationMetadata.duration?.inMilliseconds, + artUri: notificationMetadata.artUri?.toString(), + ); + } + + return _playerWith(id: playerId).setBackgroundPlayback( + BackgroundPlaybackMessage( + enableBackground: enableBackground, + notificationMetadata: metadataMessage, + ), + ); + } + + @override + Future updateNotificationMetadata( + int playerId, + NotificationMetadata notificationMetadata, + ) { + return _playerWith(id: playerId).updateNotificationMetadata( + NotificationMetadataMessage( + id: notificationMetadata.id, + title: notificationMetadata.title, + artist: notificationMetadata.artist, + album: notificationMetadata.album, + durationMs: notificationMetadata.duration?.inMilliseconds, + artUri: notificationMetadata.artUri?.toString(), + ), + ); + } + _PlayerInstance _playerWith({required int id}) { final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); @@ -384,6 +433,14 @@ class _PlayerInstance { } } + Future setBackgroundPlayback(BackgroundPlaybackMessage msg) { + return _api.setBackgroundPlayback(msg); + } + + Future updateNotificationMetadata(NotificationMetadataMessage msg) { + return _api.updateNotificationMetadata(msg); + } + Future dispose() async { _isDisposed = true; _bufferPollingTimer?.cancel(); diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 1aca7dc531dd..31002b295560 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.7), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -17,33 +17,40 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Pigeon equivalent of video_platform_interface's VideoFormat. -enum PlatformVideoFormat { dash, hls, ss } +enum PlatformVideoFormat { + dash, + hls, + ss, +} /// Pigeon equivalent of Player's playback state. /// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state -enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } +enum PlatformPlaybackState { + idle, + buffering, + ready, + ended, + unknown, +} -sealed class PlatformVideoEvent {} +sealed class PlatformVideoEvent { +} /// Sent when the video is initialized and ready to play. class InitializationEvent extends PlatformVideoEvent { @@ -67,12 +74,16 @@ class InitializationEvent extends PlatformVideoEvent { int rotationCorrection; List _toList() { - return [duration, width, height, rotationCorrection]; + return [ + duration, + width, + height, + rotationCorrection, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static InitializationEvent decode(Object result) { result as List; @@ -98,35 +109,40 @@ class InitializationEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video state changes. /// /// Corresponds to ExoPlayer's onPlaybackStateChanged. class PlaybackStateChangeEvent extends PlatformVideoEvent { - PlaybackStateChangeEvent({required this.state}); + PlaybackStateChangeEvent({ + required this.state, + }); PlatformPlaybackState state; List _toList() { - return [state]; + return [ + state, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackStateChangeEvent decode(Object result) { result as List; - return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); + return PlaybackStateChangeEvent( + state: result[0]! as PlatformPlaybackState, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlaybackStateChangeEvent || - other.runtimeType != runtimeType) { + if (other is! PlaybackStateChangeEvent || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -137,28 +153,34 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video starts or stops playing. /// /// Corresponds to ExoPlayer's onIsPlayingChanged. class IsPlayingStateEvent extends PlatformVideoEvent { - IsPlayingStateEvent({required this.isPlaying}); + IsPlayingStateEvent({ + required this.isPlaying, + }); bool isPlaying; List _toList() { - return [isPlaying]; + return [ + isPlaying, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static IsPlayingStateEvent decode(Object result) { result as List; - return IsPlayingStateEvent(isPlaying: result[0]! as bool); + return IsPlayingStateEvent( + isPlaying: result[0]! as bool, + ); } @override @@ -175,7 +197,8 @@ class IsPlayingStateEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when audio tracks change. @@ -183,22 +206,27 @@ class IsPlayingStateEvent extends PlatformVideoEvent { /// This includes when the selected audio track changes after calling selectAudioTrack. /// Corresponds to ExoPlayer's onTracksChanged. class AudioTrackChangedEvent extends PlatformVideoEvent { - AudioTrackChangedEvent({this.selectedTrackId}); + AudioTrackChangedEvent({ + this.selectedTrackId, + }); /// The ID of the newly selected audio track, if any. String? selectedTrackId; List _toList() { - return [selectedTrackId]; + return [ + selectedTrackId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackChangedEvent decode(Object result) { result as List; - return AudioTrackChangedEvent(selectedTrackId: result[0] as String?); + return AudioTrackChangedEvent( + selectedTrackId: result[0] as String?, + ); } @override @@ -215,33 +243,38 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -252,7 +285,8 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { @@ -272,20 +306,23 @@ class CreationOptions { String? userAgent; List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [ + uri, + formatHint, + httpHeaders, + userAgent, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)! - .cast(), + httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, ); } @@ -304,23 +341,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -344,11 +387,15 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class PlaybackState { - PlaybackState({required this.playPosition, required this.bufferPosition}); + PlaybackState({ + required this.playPosition, + required this.bufferPosition, + }); /// The current playback position, in milliseconds. int playPosition; @@ -357,12 +404,14 @@ class PlaybackState { int bufferPosition; List _toList() { - return [playPosition, bufferPosition]; + return [ + playPosition, + bufferPosition, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackState decode(Object result) { result as List; @@ -386,7 +435,8 @@ class PlaybackState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Represents an audio track in a video. @@ -432,8 +482,7 @@ class AudioTrackMessage { } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackMessage decode(Object result) { result as List; @@ -463,7 +512,8 @@ class AudioTrackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from ExoPlayer Format objects. @@ -513,8 +563,7 @@ class ExoPlayerAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static ExoPlayerAudioTrackData decode(Object result) { result as List; @@ -545,29 +594,32 @@ class ExoPlayerAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Container for raw audio track data from Android ExoPlayer. class NativeAudioTrackData { - NativeAudioTrackData({this.exoPlayerTracks}); + NativeAudioTrackData({ + this.exoPlayerTracks, + }); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [exoPlayerTracks]; + return [ + exoPlayerTracks, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static NativeAudioTrackData decode(Object result) { result as List; return NativeAudioTrackData( - exoPlayerTracks: (result[0] as List?) - ?.cast(), + exoPlayerTracks: (result[0] as List?)?.cast(), ); } @@ -585,9 +637,132 @@ class NativeAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Metadata for the system media notification when playing in background. +class NotificationMetadataMessage { + NotificationMetadataMessage({ + required this.id, + this.title, + this.album, + this.artist, + this.durationMs, + this.artUri, + this.smallIconResourceName, + }); + + String id; + + String? title; + + String? album; + + String? artist; + + int? durationMs; + + String? artUri; + + /// The name of a drawable resource to use as the notification's small icon. + /// Android only. If null, defaults to Media3's default icon. + String? smallIconResourceName; + + List _toList() { + return [ + id, + title, + album, + artist, + durationMs, + artUri, + smallIconResourceName, + ]; + } + + Object encode() { + return _toList(); } + + static NotificationMetadataMessage decode(Object result) { + result as List; + return NotificationMetadataMessage( + id: result[0]! as String, + title: result[1] as String?, + album: result[2] as String?, + artist: result[3] as String?, + durationMs: result[4] as int?, + artUri: result[5] as String?, + smallIconResourceName: result[6] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NotificationMetadataMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Message for configuring background playback with media notification. +class BackgroundPlaybackMessage { + BackgroundPlaybackMessage({ + required this.enableBackground, + this.notificationMetadata, + }); + + bool enableBackground; + + NotificationMetadataMessage? notificationMetadata; + + List _toList() { + return [ + enableBackground, + notificationMetadata, + ]; + } + + Object encode() { + return _toList(); } + + static BackgroundPlaybackMessage decode(Object result) { + result as List; + return BackgroundPlaybackMessage( + enableBackground: result[0]! as bool, + notificationMetadata: result[1] as NotificationMetadataMessage?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BackgroundPlaybackMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -595,45 +770,51 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoFormat) { + } else if (value is PlatformVideoFormat) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformPlaybackState) { + } else if (value is PlatformPlaybackState) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is InitializationEvent) { + } else if (value is InitializationEvent) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlaybackStateChangeEvent) { + } else if (value is PlaybackStateChangeEvent) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is IsPlayingStateEvent) { + } else if (value is IsPlayingStateEvent) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is AudioTrackChangedEvent) { + } else if (value is AudioTrackChangedEvent) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { + } else if (value is PlaybackState) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is AudioTrackMessage) { + } else if (value is AudioTrackMessage) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is ExoPlayerAudioTrackData) { + } else if (value is ExoPlayerAudioTrackData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is NativeAudioTrackData) { + } else if (value is NativeAudioTrackData) { buffer.putUint8(141); writeValue(buffer, value.encode()); + } else if (value is NotificationMetadataMessage) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is BackgroundPlaybackMessage) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -642,55 +823,53 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final value = readValue(buffer) as int?; return value == null ? null : PlatformVideoFormat.values[value]; - case 130: + case 130: final value = readValue(buffer) as int?; return value == null ? null : PlatformPlaybackState.values[value]; - case 131: + case 131: return InitializationEvent.decode(readValue(buffer)!); - case 132: + case 132: return PlaybackStateChangeEvent.decode(readValue(buffer)!); - case 133: + case 133: return IsPlayingStateEvent.decode(readValue(buffer)!); - case 134: + case 134: return AudioTrackChangedEvent.decode(readValue(buffer)!); - case 135: + case 135: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 136: + case 136: return CreationOptions.decode(readValue(buffer)!); - case 137: + case 137: return TexturePlayerIds.decode(readValue(buffer)!); - case 138: + case 138: return PlaybackState.decode(readValue(buffer)!); - case 139: + case 139: return AudioTrackMessage.decode(readValue(buffer)!); - case 140: + case 140: return ExoPlayerAudioTrackData.decode(readValue(buffer)!); - case 141: + case 141: return NativeAudioTrackData.decode(readValue(buffer)!); + case 142: + return NotificationMetadataMessage.decode(readValue(buffer)!); + case 143: + return BackgroundPlaybackMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } } } -const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( - _PigeonCodec(), -); +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); class AndroidVideoPlayerApi { /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AndroidVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -698,8 +877,7 @@ class AndroidVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -721,16 +899,13 @@ class AndroidVideoPlayerApi { } Future createForPlatformView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -751,16 +926,13 @@ class AndroidVideoPlayerApi { } Future createForTextureView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -781,16 +953,13 @@ class AndroidVideoPlayerApi { } Future dispose(int playerId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [playerId], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -806,16 +975,13 @@ class AndroidVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -831,16 +997,13 @@ class AndroidVideoPlayerApi { } Future getLookupKeyForAsset(String asset, String? packageName) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, packageName], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, packageName]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -865,13 +1028,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -880,16 +1039,13 @@ class VideoPlayerInstanceApi { /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -906,16 +1062,13 @@ class VideoPlayerInstanceApi { /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -932,16 +1085,13 @@ class VideoPlayerInstanceApi { /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -958,8 +1108,7 @@ class VideoPlayerInstanceApi { /// Begins playback if the video is not currently playing. Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -982,8 +1131,7 @@ class VideoPlayerInstanceApi { /// Pauses playback if the video is currently playing. Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1006,16 +1154,13 @@ class VideoPlayerInstanceApi { /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1032,8 +1177,7 @@ class VideoPlayerInstanceApi { /// Returns the current playback position, in milliseconds. Future getCurrentPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1061,8 +1205,7 @@ class VideoPlayerInstanceApi { /// Returns the current buffer position, in milliseconds. Future getBufferedPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1090,8 +1233,7 @@ class VideoPlayerInstanceApi { /// Gets the available audio tracks for the video. Future getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1119,16 +1261,59 @@ class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] Future selectAudioTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [groupIndex, trackIndex], + final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Configures background playback and media notification. + Future setBackgroundPlayback(BackgroundPlaybackMessage msg) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Updates the notification metadata. + Future updateNotificationMetadata(NotificationMetadataMessage msg) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.updateNotificationMetadata$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1144,15 +1329,14 @@ class VideoPlayerInstanceApi { } } -Stream videoEvents({String instanceName = ''}) { +Stream videoEvents( {String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; } - final EventChannel videoEventsChannel = EventChannel( - 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', - pigeonMethodCodec, - ); + final EventChannel videoEventsChannel = + EventChannel('dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', pigeonMethodCodec); return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { return event as PlatformVideoEvent; }); } + diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 8666b074969a..4490366e2ef5 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -148,6 +148,36 @@ class NativeAudioTrackData { List? exoPlayerTracks; } +/// Metadata for the system media notification when playing in background. +class NotificationMetadataMessage { + NotificationMetadataMessage({ + required this.id, + this.title, + this.album, + this.artist, + this.durationMs, + this.artUri, + }); + + String id; + String? title; + String? album; + String? artist; + int? durationMs; + String? artUri; +} + +/// Message for configuring background playback with media notification. +class BackgroundPlaybackMessage { + BackgroundPlaybackMessage({ + required this.enableBackground, + this.notificationMetadata, + }); + + bool enableBackground; + NotificationMetadataMessage? notificationMetadata; +} + @HostApi() abstract class AndroidVideoPlayerApi { void initialize(); @@ -192,6 +222,12 @@ abstract class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] void selectAudioTrack(int groupIndex, int trackIndex); + + /// Configures background playback and media notification. + void setBackgroundPlayback(BackgroundPlaybackMessage msg); + + /// Updates the notification metadata. + void updateNotificationMetadata(NotificationMetadataMessage msg); } @EventChannelApi() diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 359903f05de1..edaecf5f1a6d 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.3 +version: 2.10.0 environment: sdk: ^3.9.0 @@ -32,3 +32,7 @@ dev_dependencies: topics: - video - video-player +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../video_player_platform_interface} diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 810a815fddf5..1e1f853d302d 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -33,6 +33,14 @@ void main() { ); provideDummy>(Future.value()); + // Provide dummy values for background playback types + provideDummy( + BackgroundPlaybackMessage(enableBackground: false), + ); + provideDummy( + NotificationMetadataMessage(id: 'dummy'), + ); + (AndroidVideoPlayer, MockAndroidVideoPlayerApi, MockVideoPlayerInstanceApi) setUpMockPlayer({required int playerId, int? textureId}) { final pluginApi = MockAndroidVideoPlayerApi(); @@ -950,5 +958,124 @@ void main() { verify(api.selectAudioTrack(0, 1)); }); }); + + group('background playback', () { + test('isBackgroundPlaybackSupportAvailable returns true', () { + final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); + + expect(player.isBackgroundPlaybackSupportAvailable(), true); + }); + + test( + 'setBackgroundPlayback enables background without metadata', + () async { + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback(1, enableBackground: true); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, true); + expect(msg.notificationMetadata, isNull); + }, + ); + + test('setBackgroundPlayback disables background', () async { + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback(1, enableBackground: false); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, false); + }); + + test('setBackgroundPlayback passes notification metadata', () async { + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback( + 1, + enableBackground: true, + notificationMetadata: NotificationMetadata( + id: 'video_1', + title: 'Test Video', + artist: 'Test Artist', + album: 'Test Album', + duration: const Duration(minutes: 5), + artUri: Uri.parse('https://example.com/art.jpg'), + ), + ); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, true); + expect(msg.notificationMetadata, isNotNull); + expect(msg.notificationMetadata!.id, 'video_1'); + expect(msg.notificationMetadata!.title, 'Test Video'); + expect(msg.notificationMetadata!.artist, 'Test Artist'); + expect(msg.notificationMetadata!.album, 'Test Album'); + expect(msg.notificationMetadata!.durationMs, 300000); + expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); + }); + + test('updateNotificationMetadata passes metadata correctly', () async { + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when( + playerApi.updateNotificationMetadata(any), + ).thenAnswer((_) async {}); + + await player.updateNotificationMetadata( + 1, + NotificationMetadata( + id: 'video_1', + title: 'Updated Title', + artist: 'Updated Artist', + artUri: Uri.parse('https://example.com/new_art.jpg'), + ), + ); + + final VerificationResult verification = verify( + playerApi.updateNotificationMetadata(captureAny), + ); + final msg = verification.captured[0] as NotificationMetadataMessage; + expect(msg.id, 'video_1'); + expect(msg.title, 'Updated Title'); + expect(msg.artist, 'Updated Artist'); + expect(msg.artUri, 'https://example.com/new_art.jpg'); + }); + }); }); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index 212c9bde40c1..5950b0954867 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -22,6 +22,7 @@ import 'package:video_player_android/src/messages.g.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeTexturePlayerIds_0 extends _i1.SmartFake implements _i2.TexturePlayerIds { @@ -252,4 +253,24 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future setBackgroundPlayback(_i2.BackgroundPlaybackMessage? msg) => + (super.noSuchMethod( + Invocation.method(#setBackgroundPlayback, [msg]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future updateNotificationMetadata( + _i2.NotificationMetadataMessage? msg, + ) => + (super.noSuchMethod( + Invocation.method(#updateNotificationMetadata, [msg]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); } diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index b22e2b1231e9..4a6f532fb53c 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.10.0 + +* Adds background playback with system media notification support. + ## 2.9.3 * Fixes a regression where HTTP headers were ignored. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 2270120378d5..73647bfe42c5 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -6,6 +6,9 @@ #import "./include/video_player_avfoundation/FVPVideoPlayer_Internal.h" #import +#if TARGET_OS_IOS +#import +#endif #import "./include/video_player_avfoundation/AVAssetTrackUtils.h" @@ -69,6 +72,13 @@ static void FVPRemoveKeyValueObservers(NSObject *observer, @implementation FVPVideoPlayer { // Whether or not player and player item listeners have ever been registered. BOOL _listenersRegistered; + + // Background playback support + BOOL _enableBackgroundPlayback; + FVPNotificationMetadataMessage *_notificationMetadata; +#if TARGET_OS_IOS + id _timeObserver; +#endif } - (instancetype)initWithPlayerItem:(NSObject *)item @@ -165,6 +175,15 @@ - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error { } _disposed = YES; + // Clean up background playback resources +#if TARGET_OS_IOS + if (_enableBackgroundPlayback) { + [self teardownRemoteCommandCenter]; + [self clearNowPlayingInfo]; + [self removeTimeObserver]; + } +#endif + if (_listenersRegistered) { [[NSNotificationCenter defaultCenter] removeObserver:self]; FVPRemoveKeyValueObservers(self, FVPGetPlayerItemObservations(), self.player.currentItem); @@ -508,6 +527,349 @@ - (void)selectAudioTrackAtIndex:(NSInteger)trackIndex } } +- (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error { + _enableBackgroundPlayback = msg.enableBackground; + _notificationMetadata = msg.notificationMetadata; + +#if TARGET_OS_IOS + if (_enableBackgroundPlayback) { + // Configure audio session for background playback + NSError *audioError = nil; + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + [audioSession setCategory:AVAudioSessionCategoryPlayback + mode:AVAudioSessionModeMoviePlayback + options:0 + error:&audioError]; + if (audioError) { + NSLog(@"Error setting audio session category: %@", audioError); + } + + [audioSession setActive:YES error:&audioError]; + if (audioError) { + NSLog(@"Error activating audio session: %@", audioError); + } + + // iOS 15+: Enable seamless background playback without layer disconnection + if (@available(iOS 15.0, macOS 12.0, *)) { + _player.audiovisualBackgroundPlaybackPolicy = + AVPlayerAudiovisualBackgroundPlaybackPolicyContinuesIfPossible; + } + + // Set up remote command center for media controls + [self setupRemoteCommandCenter]; + + // Update Now Playing info if metadata is provided + if (_notificationMetadata) { + [self updateNowPlayingInfo]; + [self setupTimeObserver]; + } + } else { + // Disable background playback + [self teardownRemoteCommandCenter]; + [self clearNowPlayingInfo]; + [self removeTimeObserver]; + + // iOS 15+: Reset background playback policy to automatic (default) + if (@available(iOS 15.0, macOS 12.0, *)) { + _player.audiovisualBackgroundPlaybackPolicy = + AVPlayerAudiovisualBackgroundPlaybackPolicyAutomatic; + } + + // Deactivate audio session to release audio hardware + NSError *audioError = nil; + [[AVAudioSession sharedInstance] setActive:NO + withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation + error:&audioError]; + if (audioError) { + NSLog(@"Error deactivating audio session: %@", audioError); + } + } +#else + // macOS doesn't support MPNowPlayingInfoCenter in the same way + // Background audio works differently on macOS + if (_enableBackgroundPlayback) { + NSLog(@"Background playback enabled (macOS - limited support)"); + } +#endif +} + +- (void)updateNotificationMetadata:(FVPNotificationMetadataMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error { + _notificationMetadata = msg; + +#if TARGET_OS_IOS + if (_enableBackgroundPlayback && _notificationMetadata) { + [self updateNowPlayingInfo]; + } +#endif +} + +#if TARGET_OS_IOS +- (void)setupRemoteCommandCenter { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + + // Play command + [commandCenter.playCommand setEnabled:YES]; + [commandCenter.playCommand addTarget:self action:@selector(handlePlayCommand:)]; + + // Pause command + [commandCenter.pauseCommand setEnabled:YES]; + [commandCenter.pauseCommand addTarget:self action:@selector(handlePauseCommand:)]; + + // Toggle play/pause command + [commandCenter.togglePlayPauseCommand setEnabled:YES]; + [commandCenter.togglePlayPauseCommand addTarget:self action:@selector(handleTogglePlayPauseCommand:)]; + + // Change playback position command (seeking) + [commandCenter.changePlaybackPositionCommand setEnabled:YES]; + [commandCenter.changePlaybackPositionCommand addTarget:self + action:@selector(handleChangePlaybackPositionCommand:)]; + + // Skip forward command + [commandCenter.skipForwardCommand setEnabled:YES]; + commandCenter.skipForwardCommand.preferredIntervals = @[ @15 ]; + [commandCenter.skipForwardCommand addTarget:self action:@selector(handleSkipForwardCommand:)]; + + // Skip backward command + [commandCenter.skipBackwardCommand setEnabled:YES]; + commandCenter.skipBackwardCommand.preferredIntervals = @[ @15 ]; + [commandCenter.skipBackwardCommand addTarget:self action:@selector(handleSkipBackwardCommand:)]; +} + +- (MPRemoteCommandHandlerStatus)handlePlayCommand:(MPRemoteCommandEvent *)event { + _isPlaying = YES; + [self updatePlayingState]; + [self updateNowPlayingPlaybackState]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handlePauseCommand:(MPRemoteCommandEvent *)event { + _isPlaying = NO; + [self updatePlayingState]; + [self updateNowPlayingPlaybackState]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handleTogglePlayPauseCommand:(MPRemoteCommandEvent *)event { + _isPlaying = !_isPlaying; + [self updatePlayingState]; + [self updateNowPlayingPlaybackState]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handleChangePlaybackPositionCommand:(MPRemoteCommandEvent *)event { + MPChangePlaybackPositionCommandEvent *positionEvent = (MPChangePlaybackPositionCommandEvent *)event; + CMTime targetTime = CMTimeMakeWithSeconds(positionEvent.positionTime, NSEC_PER_SEC); + [_player seekToTime:targetTime + completionHandler:^(BOOL finished) { + if (finished) { + [self updateNowPlayingPlaybackState]; + } + }]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handleSkipForwardCommand:(MPRemoteCommandEvent *)event { + MPSkipIntervalCommandEvent *skipEvent = (MPSkipIntervalCommandEvent *)event; + CMTime currentTime = _player.currentTime; + CMTime skipTime = CMTimeMakeWithSeconds(skipEvent.interval, NSEC_PER_SEC); + CMTime newTime = CMTimeAdd(currentTime, skipTime); + [_player seekToTime:newTime + completionHandler:^(BOOL finished) { + if (finished) { + [self updateNowPlayingPlaybackState]; + } + }]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (MPRemoteCommandHandlerStatus)handleSkipBackwardCommand:(MPRemoteCommandEvent *)event { + MPSkipIntervalCommandEvent *skipEvent = (MPSkipIntervalCommandEvent *)event; + CMTime currentTime = _player.currentTime; + CMTime skipTime = CMTimeMakeWithSeconds(skipEvent.interval, NSEC_PER_SEC); + CMTime newTime = CMTimeSubtract(currentTime, skipTime); + [_player seekToTime:newTime + completionHandler:^(BOOL finished) { + if (finished) { + [self updateNowPlayingPlaybackState]; + } + }]; + return MPRemoteCommandHandlerStatusSuccess; +} + +- (void)teardownRemoteCommandCenter { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + + [commandCenter.playCommand removeTarget:self action:@selector(handlePlayCommand:)]; + [commandCenter.pauseCommand removeTarget:self action:@selector(handlePauseCommand:)]; + [commandCenter.togglePlayPauseCommand removeTarget:self action:@selector(handleTogglePlayPauseCommand:)]; + [commandCenter.changePlaybackPositionCommand removeTarget:self + action:@selector(handleChangePlaybackPositionCommand:)]; + [commandCenter.skipForwardCommand removeTarget:self action:@selector(handleSkipForwardCommand:)]; + [commandCenter.skipBackwardCommand removeTarget:self action:@selector(handleSkipBackwardCommand:)]; + + [commandCenter.playCommand setEnabled:NO]; + [commandCenter.pauseCommand setEnabled:NO]; + [commandCenter.togglePlayPauseCommand setEnabled:NO]; + [commandCenter.changePlaybackPositionCommand setEnabled:NO]; + [commandCenter.skipForwardCommand setEnabled:NO]; + [commandCenter.skipBackwardCommand setEnabled:NO]; +} + +- (void)updateNowPlayingInfo { + if (!_notificationMetadata) { + return; + } + + NSMutableDictionary *nowPlayingInfo = [[NSMutableDictionary alloc] init]; + + // Set title + if (_notificationMetadata.title) { + nowPlayingInfo[MPMediaItemPropertyTitle] = _notificationMetadata.title; + } + + // Set artist + if (_notificationMetadata.artist) { + nowPlayingInfo[MPMediaItemPropertyArtist] = _notificationMetadata.artist; + } + + // Set album + if (_notificationMetadata.album) { + nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = _notificationMetadata.album; + } + + // Set duration + NSNumber *durationMs = _notificationMetadata.durationMs; + if (durationMs) { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = @(durationMs.doubleValue / 1000.0); + } else { + // Use the actual video duration + CMTime duration = _player.currentItem.asset.duration; + if (CMTIME_IS_VALID(duration) && !CMTIME_IS_INDEFINITE(duration)) { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = @(CMTimeGetSeconds(duration)); + } + } + + // Set current playback position + CMTime currentTime = _player.currentTime; + if (CMTIME_IS_VALID(currentTime)) { + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(CMTimeGetSeconds(currentTime)); + } + + // Set playback rate + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(_isPlaying ? _player.rate : 0.0); + + // Load artwork asynchronously if provided + if (_notificationMetadata.artUri) { + [self loadArtworkFromUri:_notificationMetadata.artUri + completion:^(MPMediaItemArtwork *artwork) { + if (artwork) { + NSMutableDictionary *info = + [[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo mutableCopy]; + if (info) { + info[MPMediaItemPropertyArtwork] = artwork; + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = info; + } + } + }]; + } + + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo; +} + +- (void)updateNowPlayingPlaybackState { + NSMutableDictionary *nowPlayingInfo = + [[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo mutableCopy]; + if (!nowPlayingInfo) { + return; + } + + // Update elapsed time + CMTime currentTime = _player.currentTime; + if (CMTIME_IS_VALID(currentTime)) { + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(CMTimeGetSeconds(currentTime)); + } + + // Update playback rate + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(_isPlaying ? _player.rate : 0.0); + + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo; +} + +- (void)clearNowPlayingInfo { + [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil; +} + +- (void)setupTimeObserver { + // Remove existing observer if any + [self removeTimeObserver]; + + // Add periodic time observer to update Now Playing info + __weak typeof(self) weakSelf = self; + _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC) + queue:dispatch_get_main_queue() + usingBlock:^(CMTime time) { + [weakSelf updateNowPlayingPlaybackState]; + }]; +} + +- (void)removeTimeObserver { + if (_timeObserver) { + [_player removeTimeObserver:_timeObserver]; + _timeObserver = nil; + } +} + +- (void)loadArtworkFromUri:(NSString *)uriString + completion:(void (^)(MPMediaItemArtwork *_Nullable artwork))completion { + if (!uriString || uriString.length == 0) { + completion(nil); + return; + } + + NSURL *url = [NSURL URLWithString:uriString]; + if (!url) { + completion(nil); + return; + } + + // Check if it's a local file or network URL + if ([url.scheme isEqualToString:@"file"]) { + UIImage *image = [UIImage imageWithContentsOfFile:url.path]; + if (image) { + MPMediaItemArtwork *artwork = + [[MPMediaItemArtwork alloc] initWithBoundsSize:image.size + requestHandler:^UIImage *_Nonnull(CGSize size) { + return image; + }]; + completion(artwork); + } else { + completion(nil); + } + } else { + // Network URL - load asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *imageData = [NSData dataWithContentsOfURL:url]; + UIImage *image = imageData ? [UIImage imageWithData:imageData] : nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (image) { + MPMediaItemArtwork *artwork = + [[MPMediaItemArtwork alloc] initWithBoundsSize:image.size + requestHandler:^UIImage *_Nonnull(CGSize size) { + return image; + }]; + completion(artwork); + } else { + completion(nil); + } + }); + }); + } +} +#endif + #pragma mark - Private - (int64_t)duration { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index 3b2dd3952245..6f4e08320d5a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -17,46 +17,77 @@ NS_ASSUME_NONNULL_BEGIN @class FVPCreationOptions; @class FVPTexturePlayerIds; @class FVPMediaSelectionAudioTrackData; +@class FVPNotificationMetadataMessage; +@class FVPBackgroundPlaybackMessage; /// Information passed to the platform view creation. @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString *uri; -@property(nonatomic, copy) NSDictionary *httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy) NSString * uri; +@property(nonatomic, copy) NSDictionary * httpHeaders; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId; +@property(nonatomic, assign) NSInteger playerId; +@property(nonatomic, assign) NSInteger textureId; @end /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @interface FVPMediaSelectionAudioTrackData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithIndex:(NSInteger)index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL)isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle; -@property(nonatomic, assign) NSInteger index; -@property(nonatomic, copy, nullable) NSString *displayName; -@property(nonatomic, copy, nullable) NSString *languageCode; -@property(nonatomic, assign) BOOL isSelected; -@property(nonatomic, copy, nullable) NSString *commonMetadataTitle; ++ (instancetype)makeWithIndex:(NSInteger )index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL )isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle; +@property(nonatomic, assign) NSInteger index; +@property(nonatomic, copy, nullable) NSString * displayName; +@property(nonatomic, copy, nullable) NSString * languageCode; +@property(nonatomic, assign) BOOL isSelected; +@property(nonatomic, copy, nullable) NSString * commonMetadataTitle; +@end + +/// Metadata for the system media notification when playing in background. +@interface FVPNotificationMetadataMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithId:(NSString *)id + title:(nullable NSString *)title + album:(nullable NSString *)album + artist:(nullable NSString *)artist + durationMs:(nullable NSNumber *)durationMs + artUri:(nullable NSString *)artUri; +@property(nonatomic, copy) NSString * id; +@property(nonatomic, copy, nullable) NSString * title; +@property(nonatomic, copy, nullable) NSString * album; +@property(nonatomic, copy, nullable) NSString * artist; +@property(nonatomic, strong, nullable) NSNumber * durationMs; +@property(nonatomic, copy, nullable) NSString * artUri; +@end + +/// Message for configuring background playback with media notification. +@interface FVPBackgroundPlaybackMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithEnableBackground:(BOOL )enableBackground + notificationMetadata:(nullable FVPNotificationMetadataMessage *)notificationMetadata; +@property(nonatomic, assign) BOOL enableBackground; +@property(nonatomic, strong, nullable) FVPNotificationMetadataMessage * notificationMetadata; @end /// The codec used by all APIs. @@ -65,25 +96,17 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *) - createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset - package:(nullable NSString *)package - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi( - id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); + +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - id binaryMessenger, - NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -96,17 +119,14 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - (void)pauseWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSArray *)getAudioTracks: - (FlutterError *_Nullable *_Nonnull)error; -- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSArray *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error; +- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +- (void)updateNotificationMetadata:(FVPNotificationMetadataMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( - id binaryMessenger, NSObject *_Nullable api, - NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index abb8efbad50d..b21faf271131 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -50,16 +50,26 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li - (NSArray *)toList; @end +@interface FVPNotificationMetadataMessage () ++ (FVPNotificationMetadataMessage *)fromList:(NSArray *)list; ++ (nullable FVPNotificationMetadataMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPBackgroundPlaybackMessage () ++ (FVPBackgroundPlaybackMessage *)fromList:(NSArray *)list; ++ (nullable FVPBackgroundPlaybackMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger)playerId { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId { + FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -75,8 +85,8 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders { + FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; return pigeonResult; @@ -99,8 +109,9 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { - FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId { + FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -123,12 +134,12 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { @end @implementation FVPMediaSelectionAudioTrackData -+ (instancetype)makeWithIndex:(NSInteger)index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL)isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle { - FVPMediaSelectionAudioTrackData *pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; ++ (instancetype)makeWithIndex:(NSInteger )index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL )isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle { + FVPMediaSelectionAudioTrackData* pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; pigeonResult.index = index; pigeonResult.displayName = displayName; pigeonResult.languageCode = languageCode; @@ -159,19 +170,89 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li } @end +@implementation FVPNotificationMetadataMessage ++ (instancetype)makeWithId:(NSString *)id + title:(nullable NSString *)title + album:(nullable NSString *)album + artist:(nullable NSString *)artist + durationMs:(nullable NSNumber *)durationMs + artUri:(nullable NSString *)artUri { + FVPNotificationMetadataMessage* pigeonResult = [[FVPNotificationMetadataMessage alloc] init]; + pigeonResult.id = id; + pigeonResult.title = title; + pigeonResult.album = album; + pigeonResult.artist = artist; + pigeonResult.durationMs = durationMs; + pigeonResult.artUri = artUri; + return pigeonResult; +} ++ (FVPNotificationMetadataMessage *)fromList:(NSArray *)list { + FVPNotificationMetadataMessage *pigeonResult = [[FVPNotificationMetadataMessage alloc] init]; + pigeonResult.id = GetNullableObjectAtIndex(list, 0); + pigeonResult.title = GetNullableObjectAtIndex(list, 1); + pigeonResult.album = GetNullableObjectAtIndex(list, 2); + pigeonResult.artist = GetNullableObjectAtIndex(list, 3); + pigeonResult.durationMs = GetNullableObjectAtIndex(list, 4); + pigeonResult.artUri = GetNullableObjectAtIndex(list, 5); + return pigeonResult; +} ++ (nullable FVPNotificationMetadataMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPNotificationMetadataMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.id ?: [NSNull null], + self.title ?: [NSNull null], + self.album ?: [NSNull null], + self.artist ?: [NSNull null], + self.durationMs ?: [NSNull null], + self.artUri ?: [NSNull null], + ]; +} +@end + +@implementation FVPBackgroundPlaybackMessage ++ (instancetype)makeWithEnableBackground:(BOOL )enableBackground + notificationMetadata:(nullable FVPNotificationMetadataMessage *)notificationMetadata { + FVPBackgroundPlaybackMessage* pigeonResult = [[FVPBackgroundPlaybackMessage alloc] init]; + pigeonResult.enableBackground = enableBackground; + pigeonResult.notificationMetadata = notificationMetadata; + return pigeonResult; +} ++ (FVPBackgroundPlaybackMessage *)fromList:(NSArray *)list { + FVPBackgroundPlaybackMessage *pigeonResult = [[FVPBackgroundPlaybackMessage alloc] init]; + pigeonResult.enableBackground = [GetNullableObjectAtIndex(list, 0) boolValue]; + pigeonResult.notificationMetadata = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FVPBackgroundPlaybackMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPBackgroundPlaybackMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.enableBackground), + self.notificationMetadata ?: [NSNull null], + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; - case 132: + case 132: return [FVPMediaSelectionAudioTrackData fromList:[self readValue]]; + case 133: + return [FVPNotificationMetadataMessage fromList:[self readValue]]; + case 134: + return [FVPBackgroundPlaybackMessage fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -194,6 +275,12 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FVPMediaSelectionAudioTrackData class]]) { [self writeByte:132]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPNotificationMetadataMessage class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPBackgroundPlaybackMessage class]]) { + [self writeByte:134]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -215,35 +302,25 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = - [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.initialize", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", - api); + NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -254,19 +331,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForPlatformView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createPlatformViewPlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -279,25 +350,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForTextureView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createTexturePlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions - error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -305,18 +369,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.setMixWithOthers", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(setMixWithOthers:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -329,18 +388,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.getAssetUrl", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(fileURLForAssetWithName:package:error:)", - api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -354,30 +408,20 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setLooping", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setLooping:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -390,18 +434,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setVolume", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setVolume:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -414,18 +453,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setPlaybackSpeed", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(setPlaybackSpeed:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -438,17 +472,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.play", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -459,16 +489,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getPosition", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -479,42 +506,32 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.seekTo", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(seekTo:completion:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", - api); + NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position - completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.pause", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -525,18 +542,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.dispose", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(disposeWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -547,17 +559,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getAudioTracks", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", - api); + NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSArray *output = [api getAudioTracks:&error]; @@ -568,18 +576,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.selectAudioTrack", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(selectAudioTrackAtIndex:error:)", - api); + NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectAudioTrackAtIndex:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_trackIndex = [GetNullableObjectAtIndex(args, 0) integerValue]; @@ -591,4 +594,42 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setBackgroundPlayback", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setBackgroundPlayback:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setBackgroundPlayback:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPBackgroundPlaybackMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setBackgroundPlayback:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.updateNotificationMetadata", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(updateNotificationMetadata:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(updateNotificationMetadata:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPNotificationMetadataMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api updateNotificationMetadata:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 4a9153c7d267..c033c06a13dc 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -31,3 +31,7 @@ flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../../video_player_platform_interface} diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 6684d9c4c658..8359609d51c8 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -212,6 +212,55 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return true; } + @override + bool isBackgroundPlaybackSupportAvailable() { + // iOS/macOS supports background playback with media notifications + return true; + } + + @override + Future setBackgroundPlayback( + int playerId, { + required bool enableBackground, + NotificationMetadata? notificationMetadata, + }) { + NotificationMetadataMessage? metadataMessage; + if (notificationMetadata != null) { + metadataMessage = NotificationMetadataMessage( + id: notificationMetadata.id, + title: notificationMetadata.title, + artist: notificationMetadata.artist, + album: notificationMetadata.album, + durationMs: notificationMetadata.duration?.inMilliseconds, + artUri: notificationMetadata.artUri?.toString(), + ); + } + + return _playerWith(id: playerId).setBackgroundPlayback( + BackgroundPlaybackMessage( + enableBackground: enableBackground, + notificationMetadata: metadataMessage, + ), + ); + } + + @override + Future updateNotificationMetadata( + int playerId, + NotificationMetadata notificationMetadata, + ) { + return _playerWith(id: playerId).updateNotificationMetadata( + NotificationMetadataMessage( + id: notificationMetadata.id, + title: notificationMetadata.title, + artist: notificationMetadata.artist, + album: notificationMetadata.album, + durationMs: notificationMetadata.duration?.inMilliseconds, + artUri: notificationMetadata.artUri?.toString(), + ), + ); + } + @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -289,6 +338,12 @@ class _PlayerInstance { Future selectAudioTrack(int trackIndex) => _api.selectAudioTrack(trackIndex); + Future setBackgroundPlayback(BackgroundPlaybackMessage msg) => + _api.setBackgroundPlayback(msg); + + Future updateNotificationMetadata(NotificationMetadataMessage msg) => + _api.updateNotificationMetadata(msg); + Stream get videoEvents { _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen( _onStreamEvent, diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 24644d8f42d0..06b170cdff97 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -217,6 +217,112 @@ class MediaSelectionAudioTrackData { int get hashCode => Object.hashAll(_toList()); } +/// Metadata for the system media notification when playing in background. +class NotificationMetadataMessage { + NotificationMetadataMessage({ + required this.id, + this.title, + this.album, + this.artist, + this.durationMs, + this.artUri, + }); + + String id; + + String? title; + + String? album; + + String? artist; + + int? durationMs; + + String? artUri; + + List _toList() { + return [id, title, album, artist, durationMs, artUri]; + } + + Object encode() { + return _toList(); + } + + static NotificationMetadataMessage decode(Object result) { + result as List; + return NotificationMetadataMessage( + id: result[0]! as String, + title: result[1] as String?, + album: result[2] as String?, + artist: result[3] as String?, + durationMs: result[4] as int?, + artUri: result[5] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NotificationMetadataMessage || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Message for configuring background playback with media notification. +class BackgroundPlaybackMessage { + BackgroundPlaybackMessage({ + required this.enableBackground, + this.notificationMetadata, + }); + + bool enableBackground; + + NotificationMetadataMessage? notificationMetadata; + + List _toList() { + return [enableBackground, notificationMetadata]; + } + + Object encode() { + return _toList(); + } + + static BackgroundPlaybackMessage decode(Object result) { + result as List; + return BackgroundPlaybackMessage( + enableBackground: result[0]! as bool, + notificationMetadata: result[1] as NotificationMetadataMessage?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BackgroundPlaybackMessage || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -236,6 +342,12 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is MediaSelectionAudioTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is NotificationMetadataMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is BackgroundPlaybackMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -252,6 +364,10 @@ class _PigeonCodec extends StandardMessageCodec { return TexturePlayerIds.decode(readValue(buffer)!); case 132: return MediaSelectionAudioTrackData.decode(readValue(buffer)!); + case 133: + return NotificationMetadataMessage.decode(readValue(buffer)!); + case 134: + return BackgroundPlaybackMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -625,6 +741,58 @@ class VideoPlayerInstanceApi { } } + Future setBackgroundPlayback(BackgroundPlaybackMessage msg) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setBackgroundPlayback$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [msg], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future updateNotificationMetadata( + NotificationMetadataMessage msg, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.updateNotificationMetadata$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [msg], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + Future> getAudioTracks() async { final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index f49b46005307..2b70c1f65e80 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -56,6 +56,36 @@ class MediaSelectionAudioTrackData { String? commonMetadataTitle; } +/// Metadata for the system media notification when playing in background. +class NotificationMetadataMessage { + NotificationMetadataMessage({ + required this.id, + this.title, + this.album, + this.artist, + this.durationMs, + this.artUri, + }); + + String id; + String? title; + String? album; + String? artist; + int? durationMs; + String? artUri; +} + +/// Message for configuring background playback with media notification. +class BackgroundPlaybackMessage { + BackgroundPlaybackMessage({ + required this.enableBackground, + this.notificationMetadata, + }); + + bool enableBackground; + NotificationMetadataMessage? notificationMetadata; +} + @HostApi() abstract class AVFoundationVideoPlayerApi { @ObjCSelector('initialize') @@ -93,4 +123,8 @@ abstract class VideoPlayerInstanceApi { List getAudioTracks(); @ObjCSelector('selectAudioTrackAtIndex:') void selectAudioTrack(int trackIndex); + @ObjCSelector('setBackgroundPlayback:') + void setBackgroundPlayback(BackgroundPlaybackMessage msg); + @ObjCSelector('updateNotificationMetadata:') + void updateNotificationMetadata(NotificationMetadataMessage msg); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 853ce2760ae8..815b8248b918 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.3 +version: 2.10.0 environment: sdk: ^3.10.0 @@ -36,3 +36,7 @@ dev_dependencies: topics: - video - video-player +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../video_player_platform_interface} diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index c16d5bcb08e9..0ebcc5792b6e 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -20,6 +20,14 @@ import 'avfoundation_video_player_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Provide dummy values for background playback types + provideDummy( + BackgroundPlaybackMessage(enableBackground: false), + ); + provideDummy( + NotificationMetadataMessage(id: 'dummy'), + ); + ( AVFoundationVideoPlayer, MockAVFoundationVideoPlayerApi, @@ -690,5 +698,126 @@ void main() { ]), ); }); + + group('background playback', () { + test('isBackgroundPlaybackSupportAvailable returns true', () { + final (AVFoundationVideoPlayer player, _, _) = setUpMockPlayer( + playerId: 1, + ); + + expect(player.isBackgroundPlaybackSupportAvailable(), true); + }); + + test( + 'setBackgroundPlayback enables background without metadata', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback(1, enableBackground: true); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, true); + expect(msg.notificationMetadata, isNull); + }, + ); + + test('setBackgroundPlayback disables background', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback(1, enableBackground: false); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, false); + }); + + test('setBackgroundPlayback passes notification metadata', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); + + await player.setBackgroundPlayback( + 1, + enableBackground: true, + notificationMetadata: NotificationMetadata( + id: 'video_1', + title: 'Test Video', + artist: 'Test Artist', + album: 'Test Album', + duration: const Duration(minutes: 5), + artUri: Uri.parse('https://example.com/art.jpg'), + ), + ); + + final VerificationResult verification = verify( + playerApi.setBackgroundPlayback(captureAny), + ); + final msg = verification.captured[0] as BackgroundPlaybackMessage; + expect(msg.enableBackground, true); + expect(msg.notificationMetadata, isNotNull); + expect(msg.notificationMetadata!.id, 'video_1'); + expect(msg.notificationMetadata!.title, 'Test Video'); + expect(msg.notificationMetadata!.artist, 'Test Artist'); + expect(msg.notificationMetadata!.album, 'Test Album'); + expect(msg.notificationMetadata!.durationMs, 300000); + expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); + }); + + test('updateNotificationMetadata passes metadata correctly', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); + when( + playerApi.updateNotificationMetadata(any), + ).thenAnswer((_) async {}); + + await player.updateNotificationMetadata( + 1, + NotificationMetadata( + id: 'video_1', + title: 'Updated Title', + artist: 'Updated Artist', + artUri: Uri.parse('https://example.com/new_art.jpg'), + ), + ); + + final VerificationResult verification = verify( + playerApi.updateNotificationMetadata(captureAny), + ); + final msg = verification.captured[0] as NotificationMetadataMessage; + expect(msg.id, 'video_1'); + expect(msg.title, 'Updated Title'); + expect(msg.artist, 'Updated Artist'); + expect(msg.artUri, 'https://example.com/new_art.jpg'); + }); + }); }); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart index 8caf6ad8dc43..44f25dedc790 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart @@ -22,6 +22,7 @@ import 'package:video_player_avfoundation/src/messages.g.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeTexturePlayerIds_0 extends _i1.SmartFake implements _i2.TexturePlayerIds { @@ -198,4 +199,48 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future> getAudioTracks() => + (super.noSuchMethod( + Invocation.method(#getAudioTracks, []), + returnValue: + _i4.Future>.value( + <_i2.MediaSelectionAudioTrackData>[], + ), + returnValueForMissingStub: + _i4.Future>.value( + <_i2.MediaSelectionAudioTrackData>[], + ), + ) + as _i4.Future>); + + @override + _i4.Future selectAudioTrack(int? trackIndex) => + (super.noSuchMethod( + Invocation.method(#selectAudioTrack, [trackIndex]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setBackgroundPlayback(_i2.BackgroundPlaybackMessage? msg) => + (super.noSuchMethod( + Invocation.method(#setBackgroundPlayback, [msg]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future updateNotificationMetadata( + _i2.NotificationMetadataMessage? msg, + ) => + (super.noSuchMethod( + Invocation.method(#updateNotificationMetadata, [msg]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); } diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 4b44b050047a..54c3adb62111 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 6.7.0 +* Adds background playback with system media notification support. * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. ## 6.6.0 diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 1cec5f42c218..e3f794e61353 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -129,6 +129,39 @@ abstract class VideoPlayerPlatform extends PlatformInterface { throw UnimplementedError('setWebOptions() has not been implemented.'); } + /// Configures background playback and media notification. + /// + /// When [enableBackground] is true, video audio will continue playing when + /// the app is backgrounded. + /// + /// When [notificationMetadata] is provided, a system media notification will + /// be shown with playback controls and the provided metadata (title, artist, etc). + /// If [notificationMetadata] is null, no notification will be shown. + Future setBackgroundPlayback( + int playerId, { + required bool enableBackground, + NotificationMetadata? notificationMetadata, + }) { + throw UnimplementedError( + 'setBackgroundPlayback() has not been implemented.', + ); + } + + /// Updates the notification metadata. + /// + /// This updates the metadata shown in the system notification, lock screen, + /// and control center without interrupting playback. + /// + /// Only works when background playback is enabled with notification metadata. + Future updateNotificationMetadata( + int playerId, + NotificationMetadata notificationMetadata, + ) { + throw UnimplementedError( + 'updateNotificationMetadata() has not been implemented.', + ); + } + /// Gets the available audio tracks for the video. Future> getAudioTracks(int playerId) { throw UnimplementedError('getAudioTracks() has not been implemented.'); @@ -153,6 +186,23 @@ abstract class VideoPlayerPlatform extends PlatformInterface { bool isAudioTrackSupportAvailable() { return false; } + + /// Returns whether background playback with media notifications is supported + /// on this platform. + /// + /// This method allows developers to query at runtime whether the current + /// platform supports background playback with system media notifications. + /// This is useful for platforms like web where background playback may not + /// be available. + /// + /// Returns `true` if [setBackgroundPlayback] and [updateNotificationMetadata] + /// are supported, `false` otherwise. + /// + /// The default implementation returns `false`. Platform implementations + /// should override this to return `true` if they support background playback. + bool isBackgroundPlaybackSupportAvailable() { + return false; + } } class _PlaceholderImplementation extends VideoPlayerPlatform {} @@ -424,6 +474,68 @@ class DurationRange { int get hashCode => Object.hash(start, end); } +/// Metadata for the system media notification when playing in background. +/// +/// Used to display information in the system's media notification, +/// lock screen, and control center. Providing this metadata to +/// [VideoPlayerOptions.notificationMetadata] enables the notification. +@immutable +class NotificationMetadata { + /// Creates a [NotificationMetadata] with the given metadata. + const NotificationMetadata({ + required this.id, + this.title, + this.album, + this.artist, + this.duration, + this.artUri, + }); + + /// A unique identifier for this media item. + final String id; + + /// The title of this media item. + final String? title; + + /// The album this media item belongs to. + final String? album; + + /// The artist of this media item. + final String? artist; + + /// The duration of this media item. + /// + /// If not specified, the duration will be automatically detected from the + /// video source after initialization. Manual specification is useful for + /// live streams or when you want to display an estimated duration before + /// the video loads. + final Duration? duration; + + /// The URI of the artwork for this media item. + /// + /// Can be a network URL, asset path, or file path. + final Uri? artUri; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NotificationMetadata && + runtimeType == other.runtimeType && + id == other.id && + title == other.title && + album == other.album && + artist == other.artist && + duration == other.duration && + artUri == other.artUri; + + @override + int get hashCode => Object.hash(id, title, album, artist, duration, artUri); + + @override + String toString() => + '${objectRuntimeType(this, 'NotificationMetadata')}(id: $id, title: $title, album: $album, artist: $artist, duration: $duration, artUri: $artUri)'; +} + /// [VideoPlayerOptions] can be optionally used to set additional player settings @immutable class VideoPlayerOptions { @@ -435,6 +547,7 @@ class VideoPlayerOptions { VideoPlayerOptions({ this.mixWithOthers = false, this.allowBackgroundPlayback = false, + this.notificationMetadata, this.webOptions, }); @@ -449,6 +562,17 @@ class VideoPlayerOptions { /// currently no way to implement this feature in this platform). final bool mixWithOthers; + /// Metadata for the system media notification. + /// + /// When provided, a system notification will be shown with media controls + /// (play/pause, seek) and the provided metadata (title, artist, album art, etc.). + /// + /// Requires [allowBackgroundPlayback] to be true for background playback. + /// + /// Note: On Android, this requires setting up foreground service permissions. + /// On iOS, this requires enabling background audio capability. + final NotificationMetadata? notificationMetadata; + /// Additional web controls final VideoPlayerWebOptions? webOptions; } diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index b39acce19665..373ddb91f1fb 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 6.6.0 +version: 6.7.0 environment: sdk: ^3.9.0 diff --git a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart index 6af3e57f6c0f..71b857d72de6 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart @@ -10,8 +10,33 @@ void main() { final options = VideoPlayerOptions(); expect(options.allowBackgroundPlayback, false); }); + test('VideoPlayerOptions mixWithOthers defaults to false', () { final options = VideoPlayerOptions(); expect(options.mixWithOthers, false); }); + + test('VideoPlayerOptions notificationMetadata defaults to null', () { + final options = VideoPlayerOptions(); + expect(options.notificationMetadata, isNull); + }); + + test('VideoPlayerOptions accepts notificationMetadata', () { + const metadata = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + artist: 'Test Artist', + ); + final options = VideoPlayerOptions(notificationMetadata: metadata); + + expect(options.notificationMetadata, isNotNull); + expect(options.notificationMetadata!.id, 'test_id'); + expect(options.notificationMetadata!.title, 'Test Title'); + expect(options.notificationMetadata!.artist, 'Test Artist'); + }); + + test('VideoPlayerOptions webOptions defaults to null', () { + final options = VideoPlayerOptions(); + expect(options.webOptions, isNull); + }); } diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart index 2d920161ec9e..4e3b88b2a619 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -40,4 +40,110 @@ void main() { test('default implementation isAudioTrackSupportAvailable returns false', () { expect(initialInstance.isAudioTrackSupportAvailable(), false); }); + + test( + 'default implementation setBackgroundPlayback throws unimplemented', + () async { + await expectLater( + () => initialInstance.setBackgroundPlayback(1, enableBackground: true), + throwsUnimplementedError, + ); + }, + ); + + test( + 'default implementation updateNotificationMetadata throws unimplemented', + () async { + await expectLater( + () => initialInstance.updateNotificationMetadata( + 1, + const NotificationMetadata(id: 'test'), + ), + throwsUnimplementedError, + ); + }, + ); + + test( + 'default implementation isBackgroundPlaybackSupportAvailable returns false', + () { + expect(initialInstance.isBackgroundPlaybackSupportAvailable(), false); + }, + ); + + group('NotificationMetadata', () { + test('constructs with required id', () { + const metadata = NotificationMetadata(id: 'test_id'); + + expect(metadata.id, 'test_id'); + expect(metadata.title, isNull); + expect(metadata.album, isNull); + expect(metadata.artist, isNull); + expect(metadata.duration, isNull); + expect(metadata.artUri, isNull); + }); + + test('constructs with all properties', () { + final metadata = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + album: 'Test Album', + artist: 'Test Artist', + duration: const Duration(minutes: 5), + artUri: Uri.parse('https://example.com/art.jpg'), + ); + + expect(metadata.id, 'test_id'); + expect(metadata.title, 'Test Title'); + expect(metadata.album, 'Test Album'); + expect(metadata.artist, 'Test Artist'); + expect(metadata.duration, const Duration(minutes: 5)); + expect(metadata.artUri, Uri.parse('https://example.com/art.jpg')); + }); + + test('equality works correctly', () { + final metadata1 = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + artUri: Uri.parse('https://example.com/art.jpg'), + ); + final metadata2 = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + artUri: Uri.parse('https://example.com/art.jpg'), + ); + final metadata3 = NotificationMetadata( + id: 'different_id', + title: 'Test Title', + artUri: Uri.parse('https://example.com/art.jpg'), + ); + + expect(metadata1, equals(metadata2)); + expect(metadata1, isNot(equals(metadata3))); + }); + + test('hashCode is consistent with equality', () { + final metadata1 = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + artUri: Uri.parse('https://example.com/art.jpg'), + ); + final metadata2 = NotificationMetadata( + id: 'test_id', + title: 'Test Title', + artUri: Uri.parse('https://example.com/art.jpg'), + ); + + expect(metadata1.hashCode, equals(metadata2.hashCode)); + }); + + test('toString returns readable representation', () { + const metadata = NotificationMetadata(id: 'test_id', title: 'Test Title'); + + final string = metadata.toString(); + expect(string, contains('NotificationMetadata')); + expect(string, contains('test_id')); + expect(string, contains('Test Title')); + }); + }); } diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index f0bd392fd683..d7fb8487061b 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.5.0 +* Adds background playback with system media notification support. * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. ## 2.4.0 diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index ae7ab08cb17d..e530925eee89 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -18,3 +18,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../../video_player_platform_interface} diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index ecc8e427d2da..02ce51657d8b 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -169,4 +169,23 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { /// Sets the audio mode to mix with other sources (ignored). @override Future setMixWithOthers(bool mixWithOthers) => Future.value(); + + /// Returns false as background playback is not supported on web. + @override + bool isBackgroundPlaybackSupportAvailable() => false; + + /// Background playback is not supported on web (silently ignored). + @override + Future setBackgroundPlayback( + int playerId, { + required bool enableBackground, + NotificationMetadata? notificationMetadata, + }) => Future.value(); + + /// Notification metadata updates are not supported on web (silently ignored). + @override + Future updateNotificationMetadata( + int playerId, + NotificationMetadata notificationMetadata, + ) => Future.value(); } diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index b3b4739896d7..819493953170 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.4.0 +version: 2.5.0 environment: sdk: ^3.9.0 @@ -31,3 +31,7 @@ dev_dependencies: topics: - video - video-player +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + video_player_platform_interface: {path: ../video_player_platform_interface} From 77e25f5056f0f130c431102df5ee7d9ed4f55d47 Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Tue, 27 Jan 2026 22:40:26 +0200 Subject: [PATCH 2/9] [video_player] Simplify background playback null-safety check --- .../video_player/lib/video_player.dart | 272 ++++-------------- 1 file changed, 49 insertions(+), 223 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index c4c37e626a78..0849fcc0e55e 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -9,8 +9,7 @@ import 'dart:math' as math show max; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:video_player_platform_interface/video_player_platform_interface.dart' - as platform_interface; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'src/closed_caption_file.dart'; @@ -27,121 +26,10 @@ export 'package:video_player_platform_interface/video_player_platform_interface. export 'src/closed_caption_file.dart'; -/// Represents an audio track in a video with its metadata. -@immutable -class VideoAudioTrack { - /// Constructs an instance of [VideoAudioTrack]. - const VideoAudioTrack({ - required this.id, - required this.isSelected, - this.label, - this.language, - this.bitrate, - this.sampleRate, - this.channelCount, - this.codec, - }); - - /// Unique identifier for the audio track. - final String id; - - /// Human-readable label for the track. - /// - /// May be null if not available from the platform. - final String? label; - - /// Language code of the audio track (e.g., 'en', 'es', 'und'). - /// - /// May be null if not available from the platform. - final String? language; +VideoPlayerPlatform? _lastVideoPlayerPlatform; - /// Whether this track is currently selected. - final bool isSelected; - - /// Bitrate of the audio track in bits per second. - /// - /// May be null if not available from the platform. - final int? bitrate; - - /// Sample rate of the audio track in Hz. - /// - /// May be null if not available from the platform. - final int? sampleRate; - - /// Number of audio channels. - /// - /// May be null if not available from the platform. - final int? channelCount; - - /// Audio codec used (e.g., 'aac', 'mp3', 'ac3'). - /// - /// May be null if not available from the platform. - final String? codec; - - @override - bool operator ==(Object other) { - return identical(this, other) || - other is VideoAudioTrack && - runtimeType == other.runtimeType && - id == other.id && - label == other.label && - language == other.language && - isSelected == other.isSelected && - bitrate == other.bitrate && - sampleRate == other.sampleRate && - channelCount == other.channelCount && - codec == other.codec; - } - - @override - int get hashCode => Object.hash( - id, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, - ); - - @override - String toString() => - 'VideoAudioTrack(' - 'id: $id, ' - 'label: $label, ' - 'language: $language, ' - 'isSelected: $isSelected, ' - 'bitrate: $bitrate, ' - 'sampleRate: $sampleRate, ' - 'channelCount: $channelCount, ' - 'codec: $codec)'; -} - -/// Converts a platform interface [VideoAudioTrack] to the public API type. -/// -/// This internal method is used to decouple the public API from the -/// platform interface implementation. -VideoAudioTrack _convertPlatformAudioTrack( - platform_interface.VideoAudioTrack platformTrack, -) { - return VideoAudioTrack( - id: platformTrack.id, - label: platformTrack.label, - language: platformTrack.language, - isSelected: platformTrack.isSelected, - bitrate: platformTrack.bitrate, - sampleRate: platformTrack.sampleRate, - channelCount: platformTrack.channelCount, - codec: platformTrack.codec, - ); -} - -platform_interface.VideoPlayerPlatform? _lastVideoPlayerPlatform; - -platform_interface.VideoPlayerPlatform get _videoPlayerPlatform { - final platform_interface.VideoPlayerPlatform currentInstance = - platform_interface.VideoPlayerPlatform.instance; +VideoPlayerPlatform get _videoPlayerPlatform { + final VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance; if (_lastVideoPlayerPlatform != currentInstance) { // This will clear all open videos on the platform when a full restart is // performed. @@ -163,7 +51,7 @@ class VideoPlayerValue { this.position = Duration.zero, this.caption = Caption.none, this.captionOffset = Duration.zero, - this.buffered = const [], + this.buffered = const [], this.isInitialized = false, this.isPlaying = false, this.isLooping = false, @@ -211,7 +99,7 @@ class VideoPlayerValue { final Duration captionOffset; /// The currently buffered ranges. - final List buffered; + final List buffered; /// True if the video is playing. False if it's paused. final bool isPlaying; @@ -277,7 +165,7 @@ class VideoPlayerValue { Duration? position, Caption? caption, Duration? captionOffset, - List? buffered, + List? buffered, bool? isInitialized, bool? isPlaying, bool? isLooping, @@ -394,9 +282,9 @@ class VideoPlayerController extends ValueNotifier { this.package, Future? closedCaptionFile, this.videoPlayerOptions, - this.viewType = platform_interface.VideoViewType.textureView, + this.viewType = VideoViewType.textureView, }) : _closedCaptionFileFuture = closedCaptionFile, - dataSourceType = platform_interface.DataSourceType.asset, + dataSourceType = DataSourceType.asset, formatHint = null, httpHeaders = const {}, super(const VideoPlayerValue(duration: Duration.zero)); @@ -421,9 +309,9 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, this.videoPlayerOptions, this.httpHeaders = const {}, - this.viewType = platform_interface.VideoViewType.textureView, + this.viewType = VideoViewType.textureView, }) : _closedCaptionFileFuture = closedCaptionFile, - dataSourceType = platform_interface.DataSourceType.network, + dataSourceType = DataSourceType.network, package = null, super(const VideoPlayerValue(duration: Duration.zero)); @@ -442,10 +330,10 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, this.videoPlayerOptions, this.httpHeaders = const {}, - this.viewType = platform_interface.VideoViewType.textureView, + this.viewType = VideoViewType.textureView, }) : _closedCaptionFileFuture = closedCaptionFile, dataSource = url.toString(), - dataSourceType = platform_interface.DataSourceType.network, + dataSourceType = DataSourceType.network, package = null, super(const VideoPlayerValue(duration: Duration.zero)); @@ -458,10 +346,10 @@ class VideoPlayerController extends ValueNotifier { Future? closedCaptionFile, this.videoPlayerOptions, this.httpHeaders = const {}, - this.viewType = platform_interface.VideoViewType.textureView, + this.viewType = VideoViewType.textureView, }) : _closedCaptionFileFuture = closedCaptionFile, dataSource = Uri.file(file.absolute.path).toString(), - dataSourceType = platform_interface.DataSourceType.file, + dataSourceType = DataSourceType.file, package = null, formatHint = null, super(const VideoPlayerValue(duration: Duration.zero)); @@ -474,14 +362,14 @@ class VideoPlayerController extends ValueNotifier { Uri contentUri, { Future? closedCaptionFile, this.videoPlayerOptions, - this.viewType = platform_interface.VideoViewType.textureView, + this.viewType = VideoViewType.textureView, }) : assert( defaultTargetPlatform == TargetPlatform.android, 'VideoPlayerController.contentUri is only supported on Android.', ), _closedCaptionFileFuture = closedCaptionFile, dataSource = contentUri.toString(), - dataSourceType = platform_interface.DataSourceType.contentUri, + dataSourceType = DataSourceType.contentUri, package = null, formatHint = null, httpHeaders = const {}, @@ -498,14 +386,14 @@ class VideoPlayerController extends ValueNotifier { /// **Android only**. Will override the platform's generic file format /// detection with whatever is set here. - final platform_interface.VideoFormat? formatHint; + final VideoFormat? formatHint; /// Describes the type of data source this [VideoPlayerController] /// is constructed with. - final platform_interface.DataSourceType dataSourceType; + final DataSourceType dataSourceType; /// Provide additional configuration options (optional). Like setting the audio mode to mix - final platform_interface.VideoPlayerOptions? videoPlayerOptions; + final VideoPlayerOptions? videoPlayerOptions; /// Only set for [asset] videos. The package that the asset was loaded from. final String? package; @@ -513,7 +401,7 @@ class VideoPlayerController extends ValueNotifier { /// The requested display mode for the video. /// /// Platforms that do not support the request view type will ignore this. - final platform_interface.VideoViewType viewType; + final VideoViewType viewType; Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; @@ -543,35 +431,35 @@ class VideoPlayerController extends ValueNotifier { _lifeCycleObserver?.initialize(); _creatingCompleter = Completer(); - final platform_interface.DataSource dataSourceDescription; + final DataSource dataSourceDescription; switch (dataSourceType) { - case platform_interface.DataSourceType.asset: - dataSourceDescription = platform_interface.DataSource( - sourceType: platform_interface.DataSourceType.asset, + case DataSourceType.asset: + dataSourceDescription = DataSource( + sourceType: DataSourceType.asset, asset: dataSource, package: package, ); - case platform_interface.DataSourceType.network: - dataSourceDescription = platform_interface.DataSource( - sourceType: platform_interface.DataSourceType.network, + case DataSourceType.network: + dataSourceDescription = DataSource( + sourceType: DataSourceType.network, uri: dataSource, formatHint: formatHint, httpHeaders: httpHeaders, ); - case platform_interface.DataSourceType.file: - dataSourceDescription = platform_interface.DataSource( - sourceType: platform_interface.DataSourceType.file, + case DataSourceType.file: + dataSourceDescription = DataSource( + sourceType: DataSourceType.file, uri: dataSource, httpHeaders: httpHeaders, ); - case platform_interface.DataSourceType.contentUri: - dataSourceDescription = platform_interface.DataSource( - sourceType: platform_interface.DataSourceType.contentUri, + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, uri: dataSource, ); } - final creationOptions = platform_interface.VideoCreationOptions( + final creationOptions = VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, ); @@ -588,14 +476,12 @@ class VideoPlayerController extends ValueNotifier { _creatingCompleter!.complete(null); // Set up background playback if enabled - final platform_interface.VideoPlayerOptions? options = videoPlayerOptions; - final platform_interface.NotificationMetadata? metadata = - options?.notificationMetadata; - if (options != null && options.allowBackgroundPlayback) { + if (videoPlayerOptions != null && + (videoPlayerOptions?.allowBackgroundPlayback ?? false)) { await _videoPlayerPlatform.setBackgroundPlayback( _playerId, enableBackground: true, - notificationMetadata: metadata, + notificationMetadata: videoPlayerOptions?.notificationMetadata, ); } @@ -609,13 +495,13 @@ class VideoPlayerController extends ValueNotifier { ); } - void eventListener(platform_interface.VideoEvent event) { + void eventListener(VideoEvent event) { if (_isDisposed) { return; } switch (event.eventType) { - case platform_interface.VideoEventType.initialized: + case VideoEventType.initialized: value = value.copyWith( duration: event.duration, size: event.size, @@ -638,20 +524,20 @@ class VideoPlayerController extends ValueNotifier { _applyLooping(); _applyVolume(); _applyPlayPause(); - case platform_interface.VideoEventType.completed: + case VideoEventType.completed: // In this case we need to stop _timer, set isPlaying=false, and // position=value.duration. Instead of setting the values directly, // we use pause() and seekTo() to ensure the platform stops playing // and seeks to the last frame of the video. pause().then((void pauseResult) => seekTo(value.duration)); value = value.copyWith(isCompleted: true); - case platform_interface.VideoEventType.bufferingUpdate: + case VideoEventType.bufferingUpdate: value = value.copyWith(buffered: event.buffered); - case platform_interface.VideoEventType.bufferingStart: + case VideoEventType.bufferingStart: value = value.copyWith(isBuffering: true); - case platform_interface.VideoEventType.bufferingEnd: + case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); - case platform_interface.VideoEventType.isPlayingStateUpdate: + case VideoEventType.isPlayingStateUpdate: if (event.isPlaying ?? false) { value = value.copyWith( isPlaying: event.isPlaying, @@ -660,7 +546,7 @@ class VideoPlayerController extends ValueNotifier { } else { value = value.copyWith(isPlaying: event.isPlaying); } - case platform_interface.VideoEventType.unknown: + case VideoEventType.unknown: break; } } @@ -870,7 +756,7 @@ class VideoPlayerController extends ValueNotifier { /// The [notificationMetadata] should contain the updated title, artist, album, /// artwork URI, etc. Future updateNotificationMetadata( - platform_interface.NotificationMetadata notificationMetadata, + NotificationMetadata notificationMetadata, ) async { if (_isDisposedOrNotInitialized) { return; @@ -974,63 +860,6 @@ class VideoPlayerController extends ValueNotifier { } } - /// Gets the available audio tracks for the video. - /// - /// Returns a list of [VideoAudioTrack] objects containing metadata about - /// each available audio track. The list may be empty if no audio tracks - /// are available or if the video is not initialized. - /// - /// Throws an error if the video player is disposed. - Future> getAudioTracks() async { - if (_isDisposed) { - throw StateError('VideoPlayerController is disposed'); - } - if (!value.isInitialized) { - return []; - } - final List platformTracks = - await _videoPlayerPlatform.getAudioTracks(_playerId); - return platformTracks.map(_convertPlatformAudioTrack).toList(); - } - - /// Selects which audio track is chosen for playback from its [trackId] - /// - /// The [trackId] should match the ID of one of the tracks returned by - /// [getAudioTracks]. If the track ID is not found or invalid, the - /// platform may ignore the request or throw an exception. - /// - /// Throws an error if the video player is disposed or not initialized. - Future selectAudioTrack(String trackId) async { - if (_isDisposedOrNotInitialized) { - throw StateError('VideoPlayerController is disposed or not initialized'); - } - // The platform implementation (e.g., Android) will wait for the track - // selection to complete by listening to platform-specific events - await _videoPlayerPlatform.selectAudioTrack(_playerId, trackId); - } - - /// Returns whether audio track selection is supported on this platform. - /// - /// This method allows developers to query at runtime whether the current - /// platform supports audio track selection functionality. This is useful - /// for platforms like web where audio track selection may not be available. - /// - /// Returns `true` if [getAudioTracks] and [selectAudioTrack] are supported, - /// `false` otherwise. - /// - /// Example usage: - /// ```dart - /// if (controller.isAudioTrackSupportAvailable()) { - /// final tracks = await controller.getAudioTracks(); - /// // Show audio track selection UI - /// } else { - /// // Hide audio track selection UI or show unsupported message - /// } - /// ``` - bool isAudioTrackSupportAvailable() { - return _videoPlayerPlatform.isAudioTrackSupportAvailable(); - } - bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; } @@ -1115,7 +944,7 @@ class _VideoPlayerState extends State { : _VideoPlayerWithRotation( rotation: widget.controller.value.rotationCorrection, child: _videoPlayerPlatform.buildViewWithOptions( - platform_interface.VideoViewOptions(playerId: _playerId), + VideoViewOptions(playerId: _playerId), ), ); } @@ -1328,10 +1157,7 @@ class _VideoProgressIndicatorState extends State { final double maxBuffering = duration == 0.0 ? 0.0 : controller.value.buffered - .map( - (platform_interface.DurationRange range) => - range.end.inMilliseconds, - ) + .map((DurationRange range) => range.end.inMilliseconds) .fold(0, math.max) / duration; progressIndicator = Stack( From db6ae824979c5ff12da36a9e90a99c94e2a2e85e Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Wed, 28 Jan 2026 00:04:33 +0200 Subject: [PATCH 3/9] [video_player] Remove updateNotificationMetadata API --- .../video_player/lib/video_player.dart | 29 -- .../video_player/test/video_player_test.dart | 13 - .../VideoMedia3SessionService.java | 21 -- .../plugins/videoplayer/VideoPlayer.java | 9 - .../flutter/plugins/videoplayer/Messages.kt | 37 +-- .../lib/src/android_video_player.dart | 21 -- .../lib/src/messages.g.dart | 30 -- .../pigeons/messages.dart | 3 - .../test/android_video_player_test.dart | 32 -- .../test/android_video_player_test.mocks.dart | 11 - .../FVPVideoPlayer.m | 11 - .../video_player_avfoundation/messages.g.h | 1 - .../video_player_avfoundation/messages.g.m | 19 -- .../lib/src/avfoundation_video_player.dart | 20 -- .../lib/src/messages.g.dart | 273 +++++++----------- .../pigeons/messages.dart | 2 - .../test/avfoundation_video_player_test.dart | 32 -- .../avfoundation_video_player_test.mocks.dart | 11 - .../lib/video_player_platform_interface.dart | 18 +- .../video_player_platform_interface_test.dart | 13 - .../lib/video_player_web.dart | 7 - 21 files changed, 114 insertions(+), 499 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 0849fcc0e55e..d7b963d504b3 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -747,35 +747,6 @@ class VideoPlayerController extends ValueNotifier { await _applyPlaybackSpeed(); } - /// Updates the notification metadata. - /// - /// This updates the metadata shown in the system notification, lock screen, - /// and control center. Only works when background playback is enabled with - /// notification metadata. - /// - /// The [notificationMetadata] should contain the updated title, artist, album, - /// artwork URI, etc. - Future updateNotificationMetadata( - NotificationMetadata notificationMetadata, - ) async { - if (_isDisposedOrNotInitialized) { - return; - } - - if (videoPlayerOptions?.notificationMetadata == null) { - throw StateError( - 'Cannot update notification metadata when notificationMetadata was not ' - 'provided in VideoPlayerOptions. Provide notificationMetadata to enable ' - 'notifications.', - ); - } - - await _videoPlayerPlatform.updateNotificationMetadata( - _playerId, - notificationMetadata, - ); - } - /// Sets the caption offset. /// /// The [offset] will be used when getting the correct caption for a specific position. diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 45187c708e7e..888901680082 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -131,11 +131,6 @@ class FakeController extends ValueNotifier } String? selectedAudioTrackId; - - @override - Future updateNotificationMetadata( - NotificationMetadata notificationMetadata, - ) async {} } Future _loadClosedCaption() async => @@ -1923,12 +1918,4 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { }) async { calls.add('setBackgroundPlayback'); } - - @override - Future updateNotificationMetadata( - int playerId, - NotificationMetadata notificationMetadata, - ) async { - calls.add('updateNotificationMetadata'); - } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java index 28ad1caca8cc..c4dceb75d87a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java @@ -241,27 +241,6 @@ public void onPlaybackStateChanged(int playbackState) { } } - /** - * Updates the metadata on an existing MediaSession. - * - * @param textureId The texture ID of the player - * @param notificationMetadata The new metadata - */ - public void updateMediaSessionMetadata( - int textureId, @NonNull NotificationMetadataMessage notificationMetadata) { - - MediaSession session = mediaSessions.get(textureId); - if (session == null) { - Log.w(TAG, "No MediaSession found for texture: " + textureId); - return; - } - - Player player = session.getPlayer(); - if (player instanceof ExoPlayer) { - updatePlayerMetadata((ExoPlayer) player, notificationMetadata); - } - } - /** * Updates the ExoPlayer's current MediaItem with new metadata. This will automatically update the * notification. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index e32cc2ff36fe..20a5cba35c3a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -313,15 +313,6 @@ public void setBackgroundPlayback(@NonNull BackgroundPlaybackMessage msg) { } } - @Override - public void updateNotificationMetadata(@NonNull NotificationMetadataMessage msg) { - if (mediaSessionService != null && textureId >= 0) { - mediaSessionService.updateMediaSessionMetadata(textureId, msg); - } else { - Log.w(TAG, "Cannot update notification metadata: service not available"); - } - } - private void bindMediaSessionService() { if (applicationContext == null) { Log.e(TAG, "Cannot bind to MediaSessionService: no application context"); diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index 13dae5c5da80..825a051cca06 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -568,12 +568,7 @@ data class NotificationMetadataMessage( val album: String? = null, val artist: String? = null, val durationMs: Long? = null, - val artUri: String? = null, - /** - * The name of a drawable resource to use as the notification's small icon. Android only. If - * null, defaults to Media3's default icon. - */ - val smallIconResourceName: String? = null + val artUri: String? = null ) { companion object { fun fromList(pigeonVar_list: List): NotificationMetadataMessage { @@ -583,9 +578,7 @@ data class NotificationMetadataMessage( val artist = pigeonVar_list[3] as String? val durationMs = pigeonVar_list[4] as Long? val artUri = pigeonVar_list[5] as String? - val smallIconResourceName = pigeonVar_list[6] as String? - return NotificationMetadataMessage( - id, title, album, artist, durationMs, artUri, smallIconResourceName) + return NotificationMetadataMessage(id, title, album, artist, durationMs, artUri) } } @@ -597,7 +590,6 @@ data class NotificationMetadataMessage( artist, durationMs, artUri, - smallIconResourceName, ) } @@ -964,8 +956,6 @@ interface VideoPlayerInstanceApi { fun selectAudioTrack(groupIndex: Long, trackIndex: Long) /** Configures background playback and media notification. */ fun setBackgroundPlayback(msg: BackgroundPlaybackMessage) - /** Updates the notification metadata. */ - fun updateNotificationMetadata(msg: NotificationMetadataMessage) companion object { /** The codec used by VideoPlayerInstanceApi. */ @@ -1223,29 +1213,6 @@ interface VideoPlayerInstanceApi { channel.setMessageHandler(null) } } - run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.updateNotificationMetadata$separatedMessageChannelSuffix", - codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val msgArg = args[0] as NotificationMetadataMessage - val wrapped: List = - try { - api.updateNotificationMetadata(msgArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } } } } diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index c505cf4354e9..c0a26849c608 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -298,23 +298,6 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { ); } - @override - Future updateNotificationMetadata( - int playerId, - NotificationMetadata notificationMetadata, - ) { - return _playerWith(id: playerId).updateNotificationMetadata( - NotificationMetadataMessage( - id: notificationMetadata.id, - title: notificationMetadata.title, - artist: notificationMetadata.artist, - album: notificationMetadata.album, - durationMs: notificationMetadata.duration?.inMilliseconds, - artUri: notificationMetadata.artUri?.toString(), - ), - ); - } - _PlayerInstance _playerWith({required int id}) { final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); @@ -437,10 +420,6 @@ class _PlayerInstance { return _api.setBackgroundPlayback(msg); } - Future updateNotificationMetadata(NotificationMetadataMessage msg) { - return _api.updateNotificationMetadata(msg); - } - Future dispose() async { _isDisposed = true; _bufferPollingTimer?.cancel(); diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 31002b295560..126e35e1c2e5 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -650,7 +650,6 @@ class NotificationMetadataMessage { this.artist, this.durationMs, this.artUri, - this.smallIconResourceName, }); String id; @@ -665,10 +664,6 @@ class NotificationMetadataMessage { String? artUri; - /// The name of a drawable resource to use as the notification's small icon. - /// Android only. If null, defaults to Media3's default icon. - String? smallIconResourceName; - List _toList() { return [ id, @@ -677,7 +672,6 @@ class NotificationMetadataMessage { artist, durationMs, artUri, - smallIconResourceName, ]; } @@ -693,7 +687,6 @@ class NotificationMetadataMessage { artist: result[3] as String?, durationMs: result[4] as int?, artUri: result[5] as String?, - smallIconResourceName: result[6] as String?, ); } @@ -1304,29 +1297,6 @@ class VideoPlayerInstanceApi { return; } } - - /// Updates the notification metadata. - Future updateNotificationMetadata(NotificationMetadataMessage msg) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.updateNotificationMetadata$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } } Stream videoEvents( {String instanceName = ''}) { diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 4490366e2ef5..c36580ca782c 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -225,9 +225,6 @@ abstract class VideoPlayerInstanceApi { /// Configures background playback and media notification. void setBackgroundPlayback(BackgroundPlaybackMessage msg); - - /// Updates the notification metadata. - void updateNotificationMetadata(NotificationMetadataMessage msg); } @EventChannelApi() diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 1e1f853d302d..2cae3f3c918f 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -1044,38 +1044,6 @@ void main() { expect(msg.notificationMetadata!.durationMs, 300000); expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); }); - - test('updateNotificationMetadata passes metadata correctly', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when( - playerApi.updateNotificationMetadata(any), - ).thenAnswer((_) async {}); - - await player.updateNotificationMetadata( - 1, - NotificationMetadata( - id: 'video_1', - title: 'Updated Title', - artist: 'Updated Artist', - artUri: Uri.parse('https://example.com/new_art.jpg'), - ), - ); - - final VerificationResult verification = verify( - playerApi.updateNotificationMetadata(captureAny), - ); - final msg = verification.captured[0] as NotificationMetadataMessage; - expect(msg.id, 'video_1'); - expect(msg.title, 'Updated Title'); - expect(msg.artist, 'Updated Artist'); - expect(msg.artUri, 'https://example.com/new_art.jpg'); - }); }); }); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index 5950b0954867..fbcf16884f18 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -262,15 +262,4 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - - @override - _i4.Future updateNotificationMetadata( - _i2.NotificationMetadataMessage? msg, - ) => - (super.noSuchMethod( - Invocation.method(#updateNotificationMetadata, [msg]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 73647bfe42c5..835a529d77c2 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -594,17 +594,6 @@ - (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg #endif } -- (void)updateNotificationMetadata:(FVPNotificationMetadataMessage *)msg - error:(FlutterError *_Nullable *_Nonnull)error { - _notificationMetadata = msg; - -#if TARGET_OS_IOS - if (_enableBackgroundPlayback && _notificationMetadata) { - [self updateNowPlayingInfo]; - } -#endif -} - #if TARGET_OS_IOS - (void)setupRemoteCommandCenter { MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index 6f4e08320d5a..af6b670703ef 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -122,7 +122,6 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error; - (void)selectAudioTrackAtIndex:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error; - (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; -- (void)updateNotificationMetadata:(FVPNotificationMetadataMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index b21faf271131..7fb9d1d92d0a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -613,23 +613,4 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } - { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.updateNotificationMetadata", messageChannelSuffix] - binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(updateNotificationMetadata:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(updateNotificationMetadata:error:)", api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - FVPNotificationMetadataMessage *arg_msg = GetNullableObjectAtIndex(args, 0); - FlutterError *error; - [api updateNotificationMetadata:arg_msg error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } } diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 8359609d51c8..21c4aba0316a 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -244,23 +244,6 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { ); } - @override - Future updateNotificationMetadata( - int playerId, - NotificationMetadata notificationMetadata, - ) { - return _playerWith(id: playerId).updateNotificationMetadata( - NotificationMetadataMessage( - id: notificationMetadata.id, - title: notificationMetadata.title, - artist: notificationMetadata.artist, - album: notificationMetadata.album, - durationMs: notificationMetadata.duration?.inMilliseconds, - artUri: notificationMetadata.artUri?.toString(), - ), - ); - } - @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -341,9 +324,6 @@ class _PlayerInstance { Future setBackgroundPlayback(BackgroundPlaybackMessage msg) => _api.setBackgroundPlayback(msg); - Future updateNotificationMetadata(NotificationMetadataMessage msg) => - _api.updateNotificationMetadata(msg); - Stream get videoEvents { _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen( _onStreamEvent, diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 06b170cdff97..3bcc2b305afd 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -17,49 +17,49 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,30 +70,35 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({ + required this.uri, + required this.httpHeaders, + }); String uri; Map httpHeaders; List _toList() { - return [uri, httpHeaders]; + return [ + uri, + httpHeaders, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)! - .cast(), + httpHeaders: (result[1] as Map?)!.cast(), ); } @@ -111,23 +116,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -151,7 +162,8 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @@ -185,8 +197,7 @@ class MediaSelectionAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static MediaSelectionAudioTrackData decode(Object result) { result as List; @@ -202,8 +213,7 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! MediaSelectionAudioTrackData || - other.runtimeType != runtimeType) { + if (other is! MediaSelectionAudioTrackData || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -214,7 +224,8 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Metadata for the system media notification when playing in background. @@ -241,12 +252,18 @@ class NotificationMetadataMessage { String? artUri; List _toList() { - return [id, title, album, artist, durationMs, artUri]; + return [ + id, + title, + album, + artist, + durationMs, + artUri, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static NotificationMetadataMessage decode(Object result) { result as List; @@ -263,8 +280,7 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! NotificationMetadataMessage || - other.runtimeType != runtimeType) { + if (other is! NotificationMetadataMessage || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -275,7 +291,8 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Message for configuring background playback with media notification. @@ -290,12 +307,14 @@ class BackgroundPlaybackMessage { NotificationMetadataMessage? notificationMetadata; List _toList() { - return [enableBackground, notificationMetadata]; + return [ + enableBackground, + notificationMetadata, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static BackgroundPlaybackMessage decode(Object result) { result as List; @@ -308,8 +327,7 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! BackgroundPlaybackMessage || - other.runtimeType != runtimeType) { + if (other is! BackgroundPlaybackMessage || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -320,9 +338,11 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -330,22 +350,22 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionAudioTrackData) { + } else if (value is MediaSelectionAudioTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is NotificationMetadataMessage) { + } else if (value is NotificationMetadataMessage) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is BackgroundPlaybackMessage) { + } else if (value is BackgroundPlaybackMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); } else { @@ -356,17 +376,17 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 130: + case 130: return CreationOptions.decode(readValue(buffer)!); - case 131: + case 131: return TexturePlayerIds.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionAudioTrackData.decode(readValue(buffer)!); - case 133: + case 133: return NotificationMetadataMessage.decode(readValue(buffer)!); - case 134: + case 134: return BackgroundPlaybackMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -378,13 +398,9 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -392,8 +408,7 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -415,16 +430,13 @@ class AVFoundationVideoPlayerApi { } Future createForPlatformView(CreationOptions params) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [params], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -444,19 +456,14 @@ class AVFoundationVideoPlayerApi { } } - Future createForTextureView( - CreationOptions creationOptions, - ) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + Future createForTextureView(CreationOptions creationOptions) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [creationOptions], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -477,16 +484,13 @@ class AVFoundationVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -502,16 +506,13 @@ class AVFoundationVideoPlayerApi { } Future getAssetUrl(String asset, String? package) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, package], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -531,13 +532,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -545,16 +542,13 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -570,16 +564,13 @@ class VideoPlayerInstanceApi { } Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -595,16 +586,13 @@ class VideoPlayerInstanceApi { } Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -620,8 +608,7 @@ class VideoPlayerInstanceApi { } Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -643,8 +630,7 @@ class VideoPlayerInstanceApi { } Future getPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -671,16 +657,13 @@ class VideoPlayerInstanceApi { } Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -696,8 +679,7 @@ class VideoPlayerInstanceApi { } Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -719,8 +701,7 @@ class VideoPlayerInstanceApi { } Future dispose() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -766,36 +747,8 @@ class VideoPlayerInstanceApi { } } - Future updateNotificationMetadata( - NotificationMetadataMessage msg, - ) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.updateNotificationMetadata$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [msg], - ); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - Future> getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -817,22 +770,18 @@ class VideoPlayerInstanceApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)! - .cast(); + return (pigeonVar_replyList[0] as List?)!.cast(); } } Future selectAudioTrack(int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [trackIndex], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([trackIndex]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 2b70c1f65e80..9d4274e24527 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -125,6 +125,4 @@ abstract class VideoPlayerInstanceApi { void selectAudioTrack(int trackIndex); @ObjCSelector('setBackgroundPlayback:') void setBackgroundPlayback(BackgroundPlaybackMessage msg); - @ObjCSelector('updateNotificationMetadata:') - void updateNotificationMetadata(NotificationMetadataMessage msg); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index 0ebcc5792b6e..921476fb612f 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -786,38 +786,6 @@ void main() { expect(msg.notificationMetadata!.durationMs, 300000); expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); }); - - test('updateNotificationMetadata passes metadata correctly', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when( - playerApi.updateNotificationMetadata(any), - ).thenAnswer((_) async {}); - - await player.updateNotificationMetadata( - 1, - NotificationMetadata( - id: 'video_1', - title: 'Updated Title', - artist: 'Updated Artist', - artUri: Uri.parse('https://example.com/new_art.jpg'), - ), - ); - - final VerificationResult verification = verify( - playerApi.updateNotificationMetadata(captureAny), - ); - final msg = verification.captured[0] as NotificationMetadataMessage; - expect(msg.id, 'video_1'); - expect(msg.title, 'Updated Title'); - expect(msg.artist, 'Updated Artist'); - expect(msg.artUri, 'https://example.com/new_art.jpg'); - }); }); }); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart index 44f25dedc790..f109a6add075 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart @@ -232,15 +232,4 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - - @override - _i4.Future updateNotificationMetadata( - _i2.NotificationMetadataMessage? msg, - ) => - (super.noSuchMethod( - Invocation.method(#updateNotificationMetadata, [msg]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); } diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index e3f794e61353..0c6431e072d7 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -147,21 +147,6 @@ abstract class VideoPlayerPlatform extends PlatformInterface { ); } - /// Updates the notification metadata. - /// - /// This updates the metadata shown in the system notification, lock screen, - /// and control center without interrupting playback. - /// - /// Only works when background playback is enabled with notification metadata. - Future updateNotificationMetadata( - int playerId, - NotificationMetadata notificationMetadata, - ) { - throw UnimplementedError( - 'updateNotificationMetadata() has not been implemented.', - ); - } - /// Gets the available audio tracks for the video. Future> getAudioTracks(int playerId) { throw UnimplementedError('getAudioTracks() has not been implemented.'); @@ -195,8 +180,7 @@ abstract class VideoPlayerPlatform extends PlatformInterface { /// This is useful for platforms like web where background playback may not /// be available. /// - /// Returns `true` if [setBackgroundPlayback] and [updateNotificationMetadata] - /// are supported, `false` otherwise. + /// Returns `true` if [setBackgroundPlayback] is supported, `false` otherwise. /// /// The default implementation returns `false`. Platform implementations /// should override this to return `true` if they support background playback. diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart index 4e3b88b2a619..0b7e818b6708 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -51,19 +51,6 @@ void main() { }, ); - test( - 'default implementation updateNotificationMetadata throws unimplemented', - () async { - await expectLater( - () => initialInstance.updateNotificationMetadata( - 1, - const NotificationMetadata(id: 'test'), - ), - throwsUnimplementedError, - ); - }, - ); - test( 'default implementation isBackgroundPlaybackSupportAvailable returns false', () { diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 02ce51657d8b..06c12055bfc3 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -181,11 +181,4 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { required bool enableBackground, NotificationMetadata? notificationMetadata, }) => Future.value(); - - /// Notification metadata updates are not supported on web (silently ignored). - @override - Future updateNotificationMetadata( - int playerId, - NotificationMetadata notificationMetadata, - ) => Future.value(); } From cebced4ae8645d535fa5f8f1e9c209de0ac84c27 Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Wed, 28 Jan 2026 10:02:03 +0200 Subject: [PATCH 4/9] [video_player] Simplify background playback API Remove setBackgroundPlayback() and isBackgroundPlaybackSupportAvailable() methods. Background playback is now automatically enabled when notificationMetadata is provided in VideoPlayerOptions during initialization. --- .../video_player/lib/video_player.dart | 13 +- .../plugins/videoplayer/VideoPlayer.java | 7 +- .../videoplayer/VideoPlayerPlugin.java | 12 +- .../flutter/plugins/videoplayer/Messages.kt | 33 +- .../video_player_android/example/pubspec.yaml | 2 +- .../lib/src/android_video_player.dart | 58 +-- .../lib/src/messages.g.dart | 435 ++++++++---------- .../pigeons/messages.dart | 6 +- .../test/android_video_player_test.dart | 87 ---- .../test/android_video_player_test.mocks.dart | 9 - .../FVPVideoPlayer.m | 57 ++- .../FVPVideoPlayerPlugin.m | 19 +- .../FVPVideoPlayer.h | 4 + .../video_player_avfoundation/messages.g.h | 105 +++-- .../video_player_avfoundation/messages.g.m | 339 ++++++++------ .../lib/src/avfoundation_video_player.dart | 57 +-- .../lib/src/messages.g.dart | 246 +++++----- .../pigeons/messages.dart | 5 +- .../test/avfoundation_video_player_test.dart | 89 ---- .../avfoundation_video_player_test.mocks.dart | 9 - .../lib/video_player_platform_interface.dart | 55 +-- .../video_player_platform_interface_test.dart | 17 - .../video_player_web/example/pubspec.yaml | 2 +- .../lib/video_player_web.dart | 12 - 24 files changed, 751 insertions(+), 927 deletions(-) diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index d7b963d504b3..bace7b3514ae 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -462,6 +462,9 @@ class VideoPlayerController extends ValueNotifier { final creationOptions = VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, + allowBackgroundPlayback: + videoPlayerOptions?.allowBackgroundPlayback ?? false, + notificationMetadata: videoPlayerOptions?.notificationMetadata, ); if (videoPlayerOptions?.mixWithOthers != null) { @@ -475,16 +478,6 @@ class VideoPlayerController extends ValueNotifier { kUninitializedPlayerId; _creatingCompleter!.complete(null); - // Set up background playback if enabled - if (videoPlayerOptions != null && - (videoPlayerOptions?.allowBackgroundPlayback ?? false)) { - await _videoPlayerPlatform.setBackgroundPlayback( - _playerId, - enableBackground: true, - notificationMetadata: videoPlayerOptions?.notificationMetadata, - ); - } - final initializingCompleter = Completer(); // Apply the web-specific options diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 20a5cba35c3a..5ef9dd01c9ac 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -286,8 +286,11 @@ public void setBackgroundPlaybackContext(@NonNull Context context, int textureId this.textureId = textureId; } - @Override - public void setBackgroundPlayback(@NonNull BackgroundPlaybackMessage msg) { + /** + * Configures background playback and media notification. Called during player creation if + * background playback is requested. + */ + public void configureBackgroundPlayback(@NonNull BackgroundPlaybackMessage msg) { backgroundPlaybackEnabled = msg.getEnableBackground(); if (!backgroundPlaybackEnabled) { diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 880a518b4169..4a179eb2e231 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -94,7 +94,7 @@ public long createForPlatformView(@NonNull CreationOptions options) { videoAsset, sharedOptions); - registerPlayerInstance(videoPlayer, id); + registerPlayerInstance(videoPlayer, id, options.getBackgroundPlayback()); return id; } @@ -114,7 +114,7 @@ public long createForPlatformView(@NonNull CreationOptions options) { videoAsset, sharedOptions); - registerPlayerInstance(videoPlayer, id); + registerPlayerInstance(videoPlayer, id, options.getBackgroundPlayback()); return new TexturePlayerIds(id, handle.id()); } @@ -145,10 +145,16 @@ public long createForPlatformView(@NonNull CreationOptions options) { } } - private void registerPlayerInstance(VideoPlayer player, long id) { + private void registerPlayerInstance( + VideoPlayer player, long id, @Nullable BackgroundPlaybackMessage backgroundPlayback) { // Set up background playback context player.setBackgroundPlaybackContext(flutterState.applicationContext, (int) id); + // Configure background playback if requested + if (backgroundPlayback != null) { + player.configureBackgroundPlayback(backgroundPlayback); + } + // Set up the instance-specific API handler, and make sure it is removed when the player is // disposed. BinaryMessenger messenger = flutterState.binaryMessenger; diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index 825a051cca06..9ceba4d3948d 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -300,7 +300,9 @@ data class CreationOptions( val uri: String, val formatHint: PlatformVideoFormat? = null, val httpHeaders: Map, - val userAgent: String? = null + val userAgent: String? = null, + /** Background playback configuration (optional). */ + val backgroundPlayback: BackgroundPlaybackMessage? = null ) { companion object { fun fromList(pigeonVar_list: List): CreationOptions { @@ -308,7 +310,8 @@ data class CreationOptions( val formatHint = pigeonVar_list[1] as PlatformVideoFormat? val httpHeaders = pigeonVar_list[2] as Map val userAgent = pigeonVar_list[3] as String? - return CreationOptions(uri, formatHint, httpHeaders, userAgent) + val backgroundPlayback = pigeonVar_list[4] as BackgroundPlaybackMessage? + return CreationOptions(uri, formatHint, httpHeaders, userAgent, backgroundPlayback) } } @@ -318,6 +321,7 @@ data class CreationOptions( formatHint, httpHeaders, userAgent, + backgroundPlayback, ) } @@ -954,8 +958,6 @@ interface VideoPlayerInstanceApi { fun getAudioTracks(): NativeAudioTrackData /** Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] */ fun selectAudioTrack(groupIndex: Long, trackIndex: Long) - /** Configures background playback and media notification. */ - fun setBackgroundPlayback(msg: BackgroundPlaybackMessage) companion object { /** The codec used by VideoPlayerInstanceApi. */ @@ -1190,29 +1192,6 @@ interface VideoPlayerInstanceApi { channel.setMessageHandler(null) } } - run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setBackgroundPlayback$separatedMessageChannelSuffix", - codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val msgArg = args[0] as BackgroundPlaybackMessage - val wrapped: List = - try { - api.setBackgroundPlayback(msgArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } } } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 24e2db4d9774..64b8c2cfd5a1 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -37,4 +37,4 @@ flutter: # FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. # See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins dependency_overrides: - video_player_platform_interface: {path: ../../video_player_platform_interface} + video_player_platform_interface: {path: ../../../../packages/video_player/video_player_platform_interface} diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index c0a26849c608..ca290d9b832e 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -109,11 +109,33 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { if (uri == null) { throw ArgumentError('Unable to construct a video asset from $options'); } + // Set up background playback configuration if provided + BackgroundPlaybackMessage? backgroundPlayback; + if (options.allowBackgroundPlayback) { + NotificationMetadataMessage? metadataMessage; + if (options.notificationMetadata != null) { + final NotificationMetadata metadata = options.notificationMetadata!; + metadataMessage = NotificationMetadataMessage( + id: metadata.id, + title: metadata.title, + artist: metadata.artist, + album: metadata.album, + durationMs: metadata.duration?.inMilliseconds, + artUri: metadata.artUri?.toString(), + ); + } + backgroundPlayback = BackgroundPlaybackMessage( + enableBackground: true, + notificationMetadata: metadataMessage, + ); + } + final pigeonCreationOptions = CreationOptions( uri: uri, httpHeaders: httpHeaders, userAgent: userAgent, formatHint: formatHint, + backgroundPlayback: backgroundPlayback, ); final int playerId; @@ -266,38 +288,6 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return true; } - @override - bool isBackgroundPlaybackSupportAvailable() { - // Android supports background playback with media notifications - return true; - } - - @override - Future setBackgroundPlayback( - int playerId, { - required bool enableBackground, - NotificationMetadata? notificationMetadata, - }) { - NotificationMetadataMessage? metadataMessage; - if (notificationMetadata != null) { - metadataMessage = NotificationMetadataMessage( - id: notificationMetadata.id, - title: notificationMetadata.title, - artist: notificationMetadata.artist, - album: notificationMetadata.album, - durationMs: notificationMetadata.duration?.inMilliseconds, - artUri: notificationMetadata.artUri?.toString(), - ); - } - - return _playerWith(id: playerId).setBackgroundPlayback( - BackgroundPlaybackMessage( - enableBackground: enableBackground, - notificationMetadata: metadataMessage, - ), - ); - } - _PlayerInstance _playerWith({required int id}) { final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); @@ -416,10 +406,6 @@ class _PlayerInstance { } } - Future setBackgroundPlayback(BackgroundPlaybackMessage msg) { - return _api.setBackgroundPlayback(msg); - } - Future dispose() async { _isDisposed = true; _bufferPollingTimer?.cancel(); diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 126e35e1c2e5..8f91b917a49e 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -17,40 +17,33 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key]), + ); } return a == b; } - /// Pigeon equivalent of video_platform_interface's VideoFormat. -enum PlatformVideoFormat { - dash, - hls, - ss, -} +enum PlatformVideoFormat { dash, hls, ss } /// Pigeon equivalent of Player's playback state. /// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state -enum PlatformPlaybackState { - idle, - buffering, - ready, - ended, - unknown, -} +enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } -sealed class PlatformVideoEvent { -} +sealed class PlatformVideoEvent {} /// Sent when the video is initialized and ready to play. class InitializationEvent extends PlatformVideoEvent { @@ -74,16 +67,12 @@ class InitializationEvent extends PlatformVideoEvent { int rotationCorrection; List _toList() { - return [ - duration, - width, - height, - rotationCorrection, - ]; + return [duration, width, height, rotationCorrection]; } Object encode() { - return _toList(); } + return _toList(); + } static InitializationEvent decode(Object result) { result as List; @@ -109,40 +98,35 @@ class InitializationEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when the video state changes. /// /// Corresponds to ExoPlayer's onPlaybackStateChanged. class PlaybackStateChangeEvent extends PlatformVideoEvent { - PlaybackStateChangeEvent({ - required this.state, - }); + PlaybackStateChangeEvent({required this.state}); PlatformPlaybackState state; List _toList() { - return [ - state, - ]; + return [state]; } Object encode() { - return _toList(); } + return _toList(); + } static PlaybackStateChangeEvent decode(Object result) { result as List; - return PlaybackStateChangeEvent( - state: result[0]! as PlatformPlaybackState, - ); + return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlaybackStateChangeEvent || other.runtimeType != runtimeType) { + if (other is! PlaybackStateChangeEvent || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -153,34 +137,28 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when the video starts or stops playing. /// /// Corresponds to ExoPlayer's onIsPlayingChanged. class IsPlayingStateEvent extends PlatformVideoEvent { - IsPlayingStateEvent({ - required this.isPlaying, - }); + IsPlayingStateEvent({required this.isPlaying}); bool isPlaying; List _toList() { - return [ - isPlaying, - ]; + return [isPlaying]; } Object encode() { - return _toList(); } + return _toList(); + } static IsPlayingStateEvent decode(Object result) { result as List; - return IsPlayingStateEvent( - isPlaying: result[0]! as bool, - ); + return IsPlayingStateEvent(isPlaying: result[0]! as bool); } @override @@ -197,8 +175,7 @@ class IsPlayingStateEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when audio tracks change. @@ -206,27 +183,22 @@ class IsPlayingStateEvent extends PlatformVideoEvent { /// This includes when the selected audio track changes after calling selectAudioTrack. /// Corresponds to ExoPlayer's onTracksChanged. class AudioTrackChangedEvent extends PlatformVideoEvent { - AudioTrackChangedEvent({ - this.selectedTrackId, - }); + AudioTrackChangedEvent({this.selectedTrackId}); /// The ID of the newly selected audio track, if any. String? selectedTrackId; List _toList() { - return [ - selectedTrackId, - ]; + return [selectedTrackId]; } Object encode() { - return _toList(); } + return _toList(); + } static AudioTrackChangedEvent decode(Object result) { result as List; - return AudioTrackChangedEvent( - selectedTrackId: result[0] as String?, - ); + return AudioTrackChangedEvent(selectedTrackId: result[0] as String?); } @override @@ -243,38 +215,33 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({ - required this.playerId, - }); + PlatformVideoViewCreationParams({required this.playerId}); int playerId; List _toList() { - return [ - playerId, - ]; + return [playerId]; } Object encode() { - return _toList(); } + return _toList(); + } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams( - playerId: result[0]! as int, - ); + return PlatformVideoViewCreationParams(playerId: result[0]! as int); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -285,8 +252,7 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class CreationOptions { @@ -295,6 +261,7 @@ class CreationOptions { this.formatHint, required this.httpHeaders, this.userAgent, + this.backgroundPlayback, }); String uri; @@ -305,25 +272,32 @@ class CreationOptions { String? userAgent; + /// Background playback configuration (optional). + BackgroundPlaybackMessage? backgroundPlayback; + List _toList() { return [ uri, formatHint, httpHeaders, userAgent, + backgroundPlayback, ]; } Object encode() { - return _toList(); } + return _toList(); + } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)!.cast(), + httpHeaders: (result[2] as Map?)! + .cast(), userAgent: result[3] as String?, + backgroundPlayback: result[4] as BackgroundPlaybackMessage?, ); } @@ -341,29 +315,23 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class TexturePlayerIds { - TexturePlayerIds({ - required this.playerId, - required this.textureId, - }); + TexturePlayerIds({required this.playerId, required this.textureId}); int playerId; int textureId; List _toList() { - return [ - playerId, - textureId, - ]; + return [playerId, textureId]; } Object encode() { - return _toList(); } + return _toList(); + } static TexturePlayerIds decode(Object result) { result as List; @@ -387,15 +355,11 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class PlaybackState { - PlaybackState({ - required this.playPosition, - required this.bufferPosition, - }); + PlaybackState({required this.playPosition, required this.bufferPosition}); /// The current playback position, in milliseconds. int playPosition; @@ -404,14 +368,12 @@ class PlaybackState { int bufferPosition; List _toList() { - return [ - playPosition, - bufferPosition, - ]; + return [playPosition, bufferPosition]; } Object encode() { - return _toList(); } + return _toList(); + } static PlaybackState decode(Object result) { result as List; @@ -435,8 +397,7 @@ class PlaybackState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Represents an audio track in a video. @@ -482,7 +443,8 @@ class AudioTrackMessage { } Object encode() { - return _toList(); } + return _toList(); + } static AudioTrackMessage decode(Object result) { result as List; @@ -512,8 +474,7 @@ class AudioTrackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Raw audio track data from ExoPlayer Format objects. @@ -563,7 +524,8 @@ class ExoPlayerAudioTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static ExoPlayerAudioTrackData decode(Object result) { result as List; @@ -594,32 +556,29 @@ class ExoPlayerAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Container for raw audio track data from Android ExoPlayer. class NativeAudioTrackData { - NativeAudioTrackData({ - this.exoPlayerTracks, - }); + NativeAudioTrackData({this.exoPlayerTracks}); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [ - exoPlayerTracks, - ]; + return [exoPlayerTracks]; } Object encode() { - return _toList(); } + return _toList(); + } static NativeAudioTrackData decode(Object result) { result as List; return NativeAudioTrackData( - exoPlayerTracks: (result[0] as List?)?.cast(), + exoPlayerTracks: (result[0] as List?) + ?.cast(), ); } @@ -637,8 +596,7 @@ class NativeAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Metadata for the system media notification when playing in background. @@ -665,18 +623,12 @@ class NotificationMetadataMessage { String? artUri; List _toList() { - return [ - id, - title, - album, - artist, - durationMs, - artUri, - ]; + return [id, title, album, artist, durationMs, artUri]; } Object encode() { - return _toList(); } + return _toList(); + } static NotificationMetadataMessage decode(Object result) { result as List; @@ -693,7 +645,8 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! NotificationMetadataMessage || other.runtimeType != runtimeType) { + if (other is! NotificationMetadataMessage || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -704,8 +657,7 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Message for configuring background playback with media notification. @@ -720,14 +672,12 @@ class BackgroundPlaybackMessage { NotificationMetadataMessage? notificationMetadata; List _toList() { - return [ - enableBackground, - notificationMetadata, - ]; + return [enableBackground, notificationMetadata]; } Object encode() { - return _toList(); } + return _toList(); + } static BackgroundPlaybackMessage decode(Object result) { result as List; @@ -740,7 +690,8 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! BackgroundPlaybackMessage || other.runtimeType != runtimeType) { + if (other is! BackgroundPlaybackMessage || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -751,11 +702,9 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -763,49 +712,49 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoFormat) { + } else if (value is PlatformVideoFormat) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformPlaybackState) { + } else if (value is PlatformPlaybackState) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is InitializationEvent) { + } else if (value is InitializationEvent) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlaybackStateChangeEvent) { + } else if (value is PlaybackStateChangeEvent) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is IsPlayingStateEvent) { + } else if (value is IsPlayingStateEvent) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is AudioTrackChangedEvent) { + } else if (value is AudioTrackChangedEvent) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { + } else if (value is PlaybackState) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is AudioTrackMessage) { + } else if (value is AudioTrackMessage) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is ExoPlayerAudioTrackData) { + } else if (value is ExoPlayerAudioTrackData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is NativeAudioTrackData) { + } else if (value is NativeAudioTrackData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is NotificationMetadataMessage) { + } else if (value is NotificationMetadataMessage) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is BackgroundPlaybackMessage) { + } else if (value is BackgroundPlaybackMessage) { buffer.putUint8(143); writeValue(buffer, value.encode()); } else { @@ -816,37 +765,37 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final value = readValue(buffer) as int?; return value == null ? null : PlatformVideoFormat.values[value]; - case 130: + case 130: final value = readValue(buffer) as int?; return value == null ? null : PlatformPlaybackState.values[value]; - case 131: + case 131: return InitializationEvent.decode(readValue(buffer)!); - case 132: + case 132: return PlaybackStateChangeEvent.decode(readValue(buffer)!); - case 133: + case 133: return IsPlayingStateEvent.decode(readValue(buffer)!); - case 134: + case 134: return AudioTrackChangedEvent.decode(readValue(buffer)!); - case 135: + case 135: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 136: + case 136: return CreationOptions.decode(readValue(buffer)!); - case 137: + case 137: return TexturePlayerIds.decode(readValue(buffer)!); - case 138: + case 138: return PlaybackState.decode(readValue(buffer)!); - case 139: + case 139: return AudioTrackMessage.decode(readValue(buffer)!); - case 140: + case 140: return ExoPlayerAudioTrackData.decode(readValue(buffer)!); - case 141: + case 141: return NativeAudioTrackData.decode(readValue(buffer)!); - case 142: + case 142: return NotificationMetadataMessage.decode(readValue(buffer)!); - case 143: + case 143: return BackgroundPlaybackMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -854,15 +803,21 @@ class _PigeonCodec extends StandardMessageCodec { } } -const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( + _PigeonCodec(), +); class AndroidVideoPlayerApi { /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + AndroidVideoPlayerApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -870,7 +825,8 @@ class AndroidVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -892,13 +848,16 @@ class AndroidVideoPlayerApi { } Future createForPlatformView(CreationOptions options) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [options], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -919,13 +878,16 @@ class AndroidVideoPlayerApi { } Future createForTextureView(CreationOptions options) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [options], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -946,13 +908,16 @@ class AndroidVideoPlayerApi { } Future dispose(int playerId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [playerId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -968,13 +933,16 @@ class AndroidVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mixWithOthers], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -990,13 +958,16 @@ class AndroidVideoPlayerApi { } Future getLookupKeyForAsset(String asset, String? packageName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, packageName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [asset, packageName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1021,9 +992,13 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + VideoPlayerInstanceApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -1032,13 +1007,16 @@ class VideoPlayerInstanceApi { /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [looping], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1055,13 +1033,16 @@ class VideoPlayerInstanceApi { /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [volume], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1078,13 +1059,16 @@ class VideoPlayerInstanceApi { /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [speed], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1101,7 +1085,8 @@ class VideoPlayerInstanceApi { /// Begins playback if the video is not currently playing. Future play() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1124,7 +1109,8 @@ class VideoPlayerInstanceApi { /// Pauses playback if the video is currently playing. Future pause() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1147,13 +1133,16 @@ class VideoPlayerInstanceApi { /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [position], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1170,7 +1159,8 @@ class VideoPlayerInstanceApi { /// Returns the current playback position, in milliseconds. Future getCurrentPosition() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1198,7 +1188,8 @@ class VideoPlayerInstanceApi { /// Returns the current buffer position, in milliseconds. Future getBufferedPosition() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1226,7 +1217,8 @@ class VideoPlayerInstanceApi { /// Gets the available audio tracks for the video. Future getAudioTracks() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1254,36 +1246,16 @@ class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] Future selectAudioTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - - /// Configures background playback and media notification. - Future setBackgroundPlayback(BackgroundPlaybackMessage msg) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setBackgroundPlayback$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [groupIndex, trackIndex], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([msg]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1299,14 +1271,15 @@ class VideoPlayerInstanceApi { } } -Stream videoEvents( {String instanceName = ''}) { +Stream videoEvents({String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; } - final EventChannel videoEventsChannel = - EventChannel('dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', pigeonMethodCodec); + final EventChannel videoEventsChannel = EventChannel( + 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', + pigeonMethodCodec, + ); return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { return event as PlatformVideoEvent; }); } - diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index c36580ca782c..08bda2fe40ba 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -73,6 +73,9 @@ class CreationOptions { PlatformVideoFormat? formatHint; Map httpHeaders; String? userAgent; + + /// Background playback configuration (optional). + BackgroundPlaybackMessage? backgroundPlayback; } class TexturePlayerIds { @@ -222,9 +225,6 @@ abstract class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] void selectAudioTrack(int groupIndex, int trackIndex); - - /// Configures background playback and media notification. - void setBackgroundPlayback(BackgroundPlaybackMessage msg); } @EventChannelApi() diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 2cae3f3c918f..47bb0274cff7 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -958,92 +958,5 @@ void main() { verify(api.selectAudioTrack(0, 1)); }); }); - - group('background playback', () { - test('isBackgroundPlaybackSupportAvailable returns true', () { - final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); - - expect(player.isBackgroundPlaybackSupportAvailable(), true); - }); - - test( - 'setBackgroundPlayback enables background without metadata', - () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback(1, enableBackground: true); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, true); - expect(msg.notificationMetadata, isNull); - }, - ); - - test('setBackgroundPlayback disables background', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback(1, enableBackground: false); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, false); - }); - - test('setBackgroundPlayback passes notification metadata', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback( - 1, - enableBackground: true, - notificationMetadata: NotificationMetadata( - id: 'video_1', - title: 'Test Video', - artist: 'Test Artist', - album: 'Test Album', - duration: const Duration(minutes: 5), - artUri: Uri.parse('https://example.com/art.jpg'), - ), - ); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, true); - expect(msg.notificationMetadata, isNotNull); - expect(msg.notificationMetadata!.id, 'video_1'); - expect(msg.notificationMetadata!.title, 'Test Video'); - expect(msg.notificationMetadata!.artist, 'Test Artist'); - expect(msg.notificationMetadata!.album, 'Test Album'); - expect(msg.notificationMetadata!.durationMs, 300000); - expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); - }); - }); }); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index fbcf16884f18..7232580654bc 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -253,13 +253,4 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - - @override - _i4.Future setBackgroundPlayback(_i2.BackgroundPlaybackMessage? msg) => - (super.noSuchMethod( - Invocation.method(#setBackgroundPlayback, [msg]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); } diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 835a529d77c2..1ea5a8d2a7ce 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -527,8 +527,9 @@ - (void)selectAudioTrackAtIndex:(NSInteger)trackIndex } } -- (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg - error:(FlutterError *_Nullable *_Nonnull)error { +/// Configures background playback and media notification. +/// Called during player creation if background playback is requested. +- (void)configureBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg { _enableBackgroundPlayback = msg.enableBackground; _notificationMetadata = msg.notificationMetadata; @@ -578,9 +579,10 @@ - (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg // Deactivate audio session to release audio hardware NSError *audioError = nil; - [[AVAudioSession sharedInstance] setActive:NO - withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation - error:&audioError]; + [[AVAudioSession sharedInstance] + setActive:NO + withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation + error:&audioError]; if (audioError) { NSLog(@"Error deactivating audio session: %@", audioError); } @@ -608,12 +610,14 @@ - (void)setupRemoteCommandCenter { // Toggle play/pause command [commandCenter.togglePlayPauseCommand setEnabled:YES]; - [commandCenter.togglePlayPauseCommand addTarget:self action:@selector(handleTogglePlayPauseCommand:)]; + [commandCenter.togglePlayPauseCommand addTarget:self + action:@selector(handleTogglePlayPauseCommand:)]; // Change playback position command (seeking) [commandCenter.changePlaybackPositionCommand setEnabled:YES]; - [commandCenter.changePlaybackPositionCommand addTarget:self - action:@selector(handleChangePlaybackPositionCommand:)]; + [commandCenter.changePlaybackPositionCommand + addTarget:self + action:@selector(handleChangePlaybackPositionCommand:)]; // Skip forward command [commandCenter.skipForwardCommand setEnabled:YES]; @@ -648,7 +652,8 @@ - (MPRemoteCommandHandlerStatus)handleTogglePlayPauseCommand:(MPRemoteCommandEve } - (MPRemoteCommandHandlerStatus)handleChangePlaybackPositionCommand:(MPRemoteCommandEvent *)event { - MPChangePlaybackPositionCommandEvent *positionEvent = (MPChangePlaybackPositionCommandEvent *)event; + MPChangePlaybackPositionCommandEvent *positionEvent = + (MPChangePlaybackPositionCommandEvent *)event; CMTime targetTime = CMTimeMakeWithSeconds(positionEvent.positionTime, NSEC_PER_SEC); [_player seekToTime:targetTime completionHandler:^(BOOL finished) { @@ -692,11 +697,14 @@ - (void)teardownRemoteCommandCenter { [commandCenter.playCommand removeTarget:self action:@selector(handlePlayCommand:)]; [commandCenter.pauseCommand removeTarget:self action:@selector(handlePauseCommand:)]; - [commandCenter.togglePlayPauseCommand removeTarget:self action:@selector(handleTogglePlayPauseCommand:)]; - [commandCenter.changePlaybackPositionCommand removeTarget:self - action:@selector(handleChangePlaybackPositionCommand:)]; + [commandCenter.togglePlayPauseCommand removeTarget:self + action:@selector(handleTogglePlayPauseCommand:)]; + [commandCenter.changePlaybackPositionCommand + removeTarget:self + action:@selector(handleChangePlaybackPositionCommand:)]; [commandCenter.skipForwardCommand removeTarget:self action:@selector(handleSkipForwardCommand:)]; - [commandCenter.skipBackwardCommand removeTarget:self action:@selector(handleSkipBackwardCommand:)]; + [commandCenter.skipBackwardCommand removeTarget:self + action:@selector(handleSkipBackwardCommand:)]; [commandCenter.playCommand setEnabled:NO]; [commandCenter.pauseCommand setEnabled:NO]; @@ -796,11 +804,12 @@ - (void)setupTimeObserver { // Add periodic time observer to update Now Playing info __weak typeof(self) weakSelf = self; - _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC) - queue:dispatch_get_main_queue() - usingBlock:^(CMTime time) { - [weakSelf updateNowPlayingPlaybackState]; - }]; + _timeObserver = + [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0, NSEC_PER_SEC) + queue:dispatch_get_main_queue() + usingBlock:^(CMTime time) { + [weakSelf updateNowPlayingPlaybackState]; + }]; } - (void)removeTimeObserver { @@ -829,9 +838,9 @@ - (void)loadArtworkFromUri:(NSString *)uriString if (image) { MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:image.size - requestHandler:^UIImage *_Nonnull(CGSize size) { - return image; - }]; + requestHandler:^UIImage *_Nonnull(CGSize size) { + return image; + }]; completion(artwork); } else { completion(nil); @@ -846,9 +855,9 @@ - (void)loadArtworkFromUri:(NSString *)uriString if (image) { MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:image.size - requestHandler:^UIImage *_Nonnull(CGSize size) { - return image; - }]; + requestHandler:^UIImage *_Nonnull(CGSize size) { + return image; + }]; completion(artwork); } else { completion(nil); diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index a420e8397401..422bbcccca13 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -136,11 +136,17 @@ - (void)detachFromEngineForRegistrar:(NSObject *)registr } - (int64_t)configurePlayer:(FVPVideoPlayer *)player - withExtraDisposeHandler:(nullable void (^)(void))extraDisposeHandler { + withExtraDisposeHandler:(nullable void (^)(void))extraDisposeHandler + backgroundPlayback:(nullable FVPBackgroundPlaybackMessage *)backgroundPlayback { int64_t playerIdentifier = self.nextPlayerIdentifier++; self.playersByIdentifier[@(playerIdentifier)] = player; - NSObject *messenger = self.binaryMessenger; + // Configure background playback if requested + if (backgroundPlayback != nil) { + [player configureBackgroundPlayback:backgroundPlayback]; + } + + NSObject *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); @@ -224,7 +230,9 @@ - (nullable NSNumber *)createPlatformViewPlayerWithOptions:(nonnull FVPCreationO avFactory:self.avFactory viewProvider:self.viewProvider]; - return @([self configurePlayer:player withExtraDisposeHandler:nil]); + return @([self configurePlayer:player + withExtraDisposeHandler:nil + backgroundPlayback:options.backgroundPlayback]); } @catch (NSException *exception) { *error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil]; return nil; @@ -255,8 +263,9 @@ - (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions: __weak typeof(self) weakSelf = self; int64_t playerIdentifier = [self configurePlayer:player withExtraDisposeHandler:^() { - [weakSelf.textureRegistry unregisterTexture:textureIdentifier]; - }]; + [weakSelf.registrar.textures unregisterTexture:textureIdentifier]; + } + backgroundPlayback:options.backgroundPlayback]; return [FVPTexturePlayerIds makeWithPlayerId:playerIdentifier textureId:textureIdentifier]; } @catch (NSException *exception) { *error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h index 02954d7a3680..5b1a0aad83a7 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h @@ -42,6 +42,10 @@ NS_ASSUME_NONNULL_BEGIN avFactory:(id)avFactory viewProvider:(NSObject *)viewProvider; +/// Configures background playback and media notification. +/// Called during player creation if background playback is requested. +- (void)configureBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg; + @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index af6b670703ef..0754734aa8ef 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -24,42 +24,44 @@ NS_ASSUME_NONNULL_BEGIN @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger )playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString * uri; -@property(nonatomic, copy) NSDictionary * httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders + backgroundPlayback:(nullable FVPBackgroundPlaybackMessage *)backgroundPlayback; +@property(nonatomic, copy) NSString *uri; +@property(nonatomic, copy) NSDictionary *httpHeaders; +/// Background playback configuration (optional). +@property(nonatomic, strong, nullable) FVPBackgroundPlaybackMessage *backgroundPlayback; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger )playerId - textureId:(NSInteger )textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; +@property(nonatomic, assign) NSInteger playerId; +@property(nonatomic, assign) NSInteger textureId; @end /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @interface FVPMediaSelectionAudioTrackData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithIndex:(NSInteger )index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL )isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle; -@property(nonatomic, assign) NSInteger index; -@property(nonatomic, copy, nullable) NSString * displayName; -@property(nonatomic, copy, nullable) NSString * languageCode; -@property(nonatomic, assign) BOOL isSelected; -@property(nonatomic, copy, nullable) NSString * commonMetadataTitle; ++ (instancetype)makeWithIndex:(NSInteger)index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL)isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle; +@property(nonatomic, assign) NSInteger index; +@property(nonatomic, copy, nullable) NSString *displayName; +@property(nonatomic, copy, nullable) NSString *languageCode; +@property(nonatomic, assign) BOOL isSelected; +@property(nonatomic, copy, nullable) NSString *commonMetadataTitle; @end /// Metadata for the system media notification when playing in background. @@ -67,27 +69,28 @@ NS_ASSUME_NONNULL_BEGIN /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithId:(NSString *)id - title:(nullable NSString *)title - album:(nullable NSString *)album - artist:(nullable NSString *)artist - durationMs:(nullable NSNumber *)durationMs - artUri:(nullable NSString *)artUri; -@property(nonatomic, copy) NSString * id; -@property(nonatomic, copy, nullable) NSString * title; -@property(nonatomic, copy, nullable) NSString * album; -@property(nonatomic, copy, nullable) NSString * artist; -@property(nonatomic, strong, nullable) NSNumber * durationMs; -@property(nonatomic, copy, nullable) NSString * artUri; + title:(nullable NSString *)title + album:(nullable NSString *)album + artist:(nullable NSString *)artist + durationMs:(nullable NSNumber *)durationMs + artUri:(nullable NSString *)artUri; +@property(nonatomic, copy) NSString *id; +@property(nonatomic, copy, nullable) NSString *title; +@property(nonatomic, copy, nullable) NSString *album; +@property(nonatomic, copy, nullable) NSString *artist; +@property(nonatomic, strong, nullable) NSNumber *durationMs; +@property(nonatomic, copy, nullable) NSString *artUri; @end /// Message for configuring background playback with media notification. @interface FVPBackgroundPlaybackMessage : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithEnableBackground:(BOOL )enableBackground - notificationMetadata:(nullable FVPNotificationMetadataMessage *)notificationMetadata; -@property(nonatomic, assign) BOOL enableBackground; -@property(nonatomic, strong, nullable) FVPNotificationMetadataMessage * notificationMetadata; ++ (instancetype)makeWithEnableBackground:(BOOL)enableBackground + notificationMetadata: + (nullable FVPNotificationMetadataMessage *)notificationMetadata; +@property(nonatomic, assign) BOOL enableBackground; +@property(nonatomic, strong, nullable) FVPNotificationMetadataMessage *notificationMetadata; @end /// The codec used by all APIs. @@ -96,17 +99,25 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params + error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *) + createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions + error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset + package:(nullable NSString *)package + error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); - -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); +extern void SetUpFVPAVFoundationVideoPlayerApi( + id binaryMessenger, + NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( + id binaryMessenger, + NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -119,13 +130,17 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error; -- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSArray *)getAudioTracks: + (FlutterError *_Nullable *_Nonnull)error; +- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex + error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, + NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( + id binaryMessenger, NSObject *_Nullable api, + NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 7fb9d1d92d0a..7ccfbcc728ae 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -63,13 +63,15 @@ + (nullable FVPBackgroundPlaybackMessage *)nullableFromList:(NSArray *)list; @end @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger )playerId { - FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId { + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -85,16 +87,19 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders + backgroundPlayback:(nullable FVPBackgroundPlaybackMessage *)backgroundPlayback { + FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; + pigeonResult.backgroundPlayback = backgroundPlayback; return pigeonResult; } + (FVPCreationOptions *)fromList:(NSArray *)list { FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = GetNullableObjectAtIndex(list, 0); pigeonResult.httpHeaders = GetNullableObjectAtIndex(list, 1); + pigeonResult.backgroundPlayback = GetNullableObjectAtIndex(list, 2); return pigeonResult; } + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @@ -104,14 +109,14 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { return @[ self.uri ?: [NSNull null], self.httpHeaders ?: [NSNull null], + self.backgroundPlayback ?: [NSNull null], ]; } @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger )playerId - textureId:(NSInteger )textureId { - FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { + FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -134,12 +139,12 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { @end @implementation FVPMediaSelectionAudioTrackData -+ (instancetype)makeWithIndex:(NSInteger )index - displayName:(nullable NSString *)displayName - languageCode:(nullable NSString *)languageCode - isSelected:(BOOL )isSelected - commonMetadataTitle:(nullable NSString *)commonMetadataTitle { - FVPMediaSelectionAudioTrackData* pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; ++ (instancetype)makeWithIndex:(NSInteger)index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL)isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle { + FVPMediaSelectionAudioTrackData *pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; pigeonResult.index = index; pigeonResult.displayName = displayName; pigeonResult.languageCode = languageCode; @@ -172,12 +177,12 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li @implementation FVPNotificationMetadataMessage + (instancetype)makeWithId:(NSString *)id - title:(nullable NSString *)title - album:(nullable NSString *)album - artist:(nullable NSString *)artist - durationMs:(nullable NSNumber *)durationMs - artUri:(nullable NSString *)artUri { - FVPNotificationMetadataMessage* pigeonResult = [[FVPNotificationMetadataMessage alloc] init]; + title:(nullable NSString *)title + album:(nullable NSString *)album + artist:(nullable NSString *)artist + durationMs:(nullable NSNumber *)durationMs + artUri:(nullable NSString *)artUri { + FVPNotificationMetadataMessage *pigeonResult = [[FVPNotificationMetadataMessage alloc] init]; pigeonResult.id = id; pigeonResult.title = title; pigeonResult.album = album; @@ -212,9 +217,10 @@ + (nullable FVPNotificationMetadataMessage *)nullableFromList:(NSArray *)lis @end @implementation FVPBackgroundPlaybackMessage -+ (instancetype)makeWithEnableBackground:(BOOL )enableBackground - notificationMetadata:(nullable FVPNotificationMetadataMessage *)notificationMetadata { - FVPBackgroundPlaybackMessage* pigeonResult = [[FVPBackgroundPlaybackMessage alloc] init]; ++ (instancetype)makeWithEnableBackground:(BOOL)enableBackground + notificationMetadata: + (nullable FVPNotificationMetadataMessage *)notificationMetadata { + FVPBackgroundPlaybackMessage *pigeonResult = [[FVPBackgroundPlaybackMessage alloc] init]; pigeonResult.enableBackground = enableBackground; pigeonResult.notificationMetadata = notificationMetadata; return pigeonResult; @@ -241,17 +247,17 @@ @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; - case 132: + case 132: return [FVPMediaSelectionAudioTrackData fromList:[self readValue]]; - case 133: + case 133: return [FVPNotificationMetadataMessage fromList:[self readValue]]; - case 134: + case 134: return [FVPBackgroundPlaybackMessage fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -302,25 +308,35 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = + [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, + NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.initialize", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); + NSCAssert([api respondsToSelector:@selector(initialize:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -331,13 +347,19 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.createForPlatformView", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(createPlatformViewPlayerWithOptions:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -350,18 +372,25 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.createForTextureView", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(createTexturePlayerWithOptions:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions + error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -369,13 +398,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.setMixWithOthers", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setMixWithOthers:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -388,13 +422,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.getAssetUrl", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(fileURLForAssetWithName:package:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -408,20 +447,30 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, + NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setLooping", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setLooping:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -434,13 +483,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setVolume", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setVolume:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -453,13 +507,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setPlaybackSpeed", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " + @"@selector(setPlaybackSpeed:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -472,13 +531,17 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.play", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -489,13 +552,16 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.getPosition", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -506,32 +572,42 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.seekTo", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); + NSCAssert( + [api respondsToSelector:@selector(seekTo:completion:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.pause", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -542,13 +618,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.dispose", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); + NSCAssert( + [api respondsToSelector:@selector(disposeWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -559,13 +640,17 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.getAudioTracks", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", api); + NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSArray *output = [api getAudioTracks:&error]; @@ -576,13 +661,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.selectAudioTrack", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectAudioTrackAtIndex:error:)", api); + NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " + @"@selector(selectAudioTrackAtIndex:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_trackIndex = [GetNullableObjectAtIndex(args, 0) integerValue]; @@ -594,23 +684,4 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } - { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setBackgroundPlayback", messageChannelSuffix] - binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(setBackgroundPlayback:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setBackgroundPlayback:error:)", api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - FVPBackgroundPlaybackMessage *arg_msg = GetNullableObjectAtIndex(args, 0); - FlutterError *error; - [api setBackgroundPlayback:arg_msg error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } } diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 21c4aba0316a..040a7d46e8ab 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -92,9 +92,31 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { if (uri == null) { throw ArgumentError('Unable to construct a video asset from $options'); } + // Set up background playback configuration if provided + BackgroundPlaybackMessage? backgroundPlayback; + if (options.allowBackgroundPlayback) { + NotificationMetadataMessage? metadataMessage; + if (options.notificationMetadata != null) { + final NotificationMetadata metadata = options.notificationMetadata!; + metadataMessage = NotificationMetadataMessage( + id: metadata.id, + title: metadata.title, + artist: metadata.artist, + album: metadata.album, + durationMs: metadata.duration?.inMilliseconds, + artUri: metadata.artUri?.toString(), + ); + } + backgroundPlayback = BackgroundPlaybackMessage( + enableBackground: true, + notificationMetadata: metadataMessage, + ); + } + final pigeonCreationOptions = CreationOptions( uri: uri, httpHeaders: dataSource.httpHeaders, + backgroundPlayback: backgroundPlayback, ); final int playerId; @@ -212,38 +234,6 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return true; } - @override - bool isBackgroundPlaybackSupportAvailable() { - // iOS/macOS supports background playback with media notifications - return true; - } - - @override - Future setBackgroundPlayback( - int playerId, { - required bool enableBackground, - NotificationMetadata? notificationMetadata, - }) { - NotificationMetadataMessage? metadataMessage; - if (notificationMetadata != null) { - metadataMessage = NotificationMetadataMessage( - id: notificationMetadata.id, - title: notificationMetadata.title, - artist: notificationMetadata.artist, - album: notificationMetadata.album, - durationMs: notificationMetadata.duration?.inMilliseconds, - artUri: notificationMetadata.artUri?.toString(), - ); - } - - return _playerWith(id: playerId).setBackgroundPlayback( - BackgroundPlaybackMessage( - enableBackground: enableBackground, - notificationMetadata: metadataMessage, - ), - ); - } - @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -321,9 +311,6 @@ class _PlayerInstance { Future selectAudioTrack(int trackIndex) => _api.selectAudioTrack(trackIndex); - Future setBackgroundPlayback(BackgroundPlaybackMessage msg) => - _api.setBackgroundPlayback(msg); - Stream get videoEvents { _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen( _onStreamEvent, diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 3bcc2b305afd..3422b21eb021 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -17,49 +17,49 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key]), + ); } return a == b; } - /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({ - required this.playerId, - }); + PlatformVideoViewCreationParams({required this.playerId}); int playerId; List _toList() { - return [ - playerId, - ]; + return [playerId]; } Object encode() { - return _toList(); } + return _toList(); + } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams( - playerId: result[0]! as int, - ); + return PlatformVideoViewCreationParams(playerId: result[0]! as int); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,35 +70,38 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class CreationOptions { CreationOptions({ required this.uri, required this.httpHeaders, + this.backgroundPlayback, }); String uri; Map httpHeaders; + /// Background playback configuration (optional). + BackgroundPlaybackMessage? backgroundPlayback; + List _toList() { - return [ - uri, - httpHeaders, - ]; + return [uri, httpHeaders, backgroundPlayback]; } Object encode() { - return _toList(); } + return _toList(); + } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)!.cast(), + httpHeaders: (result[1] as Map?)! + .cast(), + backgroundPlayback: result[2] as BackgroundPlaybackMessage?, ); } @@ -116,29 +119,23 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class TexturePlayerIds { - TexturePlayerIds({ - required this.playerId, - required this.textureId, - }); + TexturePlayerIds({required this.playerId, required this.textureId}); int playerId; int textureId; List _toList() { - return [ - playerId, - textureId, - ]; + return [playerId, textureId]; } Object encode() { - return _toList(); } + return _toList(); + } static TexturePlayerIds decode(Object result) { result as List; @@ -162,8 +159,7 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @@ -197,7 +193,8 @@ class MediaSelectionAudioTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static MediaSelectionAudioTrackData decode(Object result) { result as List; @@ -213,7 +210,8 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! MediaSelectionAudioTrackData || other.runtimeType != runtimeType) { + if (other is! MediaSelectionAudioTrackData || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -224,8 +222,7 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Metadata for the system media notification when playing in background. @@ -252,18 +249,12 @@ class NotificationMetadataMessage { String? artUri; List _toList() { - return [ - id, - title, - album, - artist, - durationMs, - artUri, - ]; + return [id, title, album, artist, durationMs, artUri]; } Object encode() { - return _toList(); } + return _toList(); + } static NotificationMetadataMessage decode(Object result) { result as List; @@ -280,7 +271,8 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! NotificationMetadataMessage || other.runtimeType != runtimeType) { + if (other is! NotificationMetadataMessage || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -291,8 +283,7 @@ class NotificationMetadataMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Message for configuring background playback with media notification. @@ -307,14 +298,12 @@ class BackgroundPlaybackMessage { NotificationMetadataMessage? notificationMetadata; List _toList() { - return [ - enableBackground, - notificationMetadata, - ]; + return [enableBackground, notificationMetadata]; } Object encode() { - return _toList(); } + return _toList(); + } static BackgroundPlaybackMessage decode(Object result) { result as List; @@ -327,7 +316,8 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! BackgroundPlaybackMessage || other.runtimeType != runtimeType) { + if (other is! BackgroundPlaybackMessage || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -338,11 +328,9 @@ class BackgroundPlaybackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -350,22 +338,22 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionAudioTrackData) { + } else if (value is MediaSelectionAudioTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is NotificationMetadataMessage) { + } else if (value is NotificationMetadataMessage) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is BackgroundPlaybackMessage) { + } else if (value is BackgroundPlaybackMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); } else { @@ -376,17 +364,17 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 130: + case 130: return CreationOptions.decode(readValue(buffer)!); - case 131: + case 131: return TexturePlayerIds.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionAudioTrackData.decode(readValue(buffer)!); - case 133: + case 133: return NotificationMetadataMessage.decode(readValue(buffer)!); - case 134: + case 134: return BackgroundPlaybackMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -398,9 +386,13 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + AVFoundationVideoPlayerApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -408,7 +400,8 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -430,13 +423,16 @@ class AVFoundationVideoPlayerApi { } Future createForPlatformView(CreationOptions params) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [params], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -456,14 +452,19 @@ class AVFoundationVideoPlayerApi { } } - Future createForTextureView(CreationOptions creationOptions) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + Future createForTextureView( + CreationOptions creationOptions, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [creationOptions], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -484,13 +485,16 @@ class AVFoundationVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mixWithOthers], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -506,13 +510,16 @@ class AVFoundationVideoPlayerApi { } Future getAssetUrl(String asset, String? package) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [asset, package], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -532,9 +539,13 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + VideoPlayerInstanceApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -542,13 +553,16 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [looping], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -564,13 +578,16 @@ class VideoPlayerInstanceApi { } Future setVolume(double volume) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [volume], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -586,13 +603,16 @@ class VideoPlayerInstanceApi { } Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [speed], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -608,7 +628,8 @@ class VideoPlayerInstanceApi { } Future play() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -630,7 +651,8 @@ class VideoPlayerInstanceApi { } Future getPosition() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -657,13 +679,16 @@ class VideoPlayerInstanceApi { } Future seekTo(int position) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [position], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -679,7 +704,8 @@ class VideoPlayerInstanceApi { } Future pause() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -701,7 +727,8 @@ class VideoPlayerInstanceApi { } Future dispose() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -748,7 +775,8 @@ class VideoPlayerInstanceApi { } Future> getAudioTracks() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -770,18 +798,22 @@ class VideoPlayerInstanceApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as List?)! + .cast(); } } Future selectAudioTrack(int trackIndex) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([trackIndex]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [trackIndex], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 9d4274e24527..52755d544dc5 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -30,6 +30,9 @@ class CreationOptions { String uri; Map httpHeaders; + + /// Background playback configuration (optional). + BackgroundPlaybackMessage? backgroundPlayback; } class TexturePlayerIds { @@ -123,6 +126,4 @@ abstract class VideoPlayerInstanceApi { List getAudioTracks(); @ObjCSelector('selectAudioTrackAtIndex:') void selectAudioTrack(int trackIndex); - @ObjCSelector('setBackgroundPlayback:') - void setBackgroundPlayback(BackgroundPlaybackMessage msg); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index 921476fb612f..b4d81ab2939e 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -698,94 +698,5 @@ void main() { ]), ); }); - - group('background playback', () { - test('isBackgroundPlaybackSupportAvailable returns true', () { - final (AVFoundationVideoPlayer player, _, _) = setUpMockPlayer( - playerId: 1, - ); - - expect(player.isBackgroundPlaybackSupportAvailable(), true); - }); - - test( - 'setBackgroundPlayback enables background without metadata', - () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback(1, enableBackground: true); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, true); - expect(msg.notificationMetadata, isNull); - }, - ); - - test('setBackgroundPlayback disables background', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback(1, enableBackground: false); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, false); - }); - - test('setBackgroundPlayback passes notification metadata', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); - when(playerApi.setBackgroundPlayback(any)).thenAnswer((_) async {}); - - await player.setBackgroundPlayback( - 1, - enableBackground: true, - notificationMetadata: NotificationMetadata( - id: 'video_1', - title: 'Test Video', - artist: 'Test Artist', - album: 'Test Album', - duration: const Duration(minutes: 5), - artUri: Uri.parse('https://example.com/art.jpg'), - ), - ); - - final VerificationResult verification = verify( - playerApi.setBackgroundPlayback(captureAny), - ); - final msg = verification.captured[0] as BackgroundPlaybackMessage; - expect(msg.enableBackground, true); - expect(msg.notificationMetadata, isNotNull); - expect(msg.notificationMetadata!.id, 'video_1'); - expect(msg.notificationMetadata!.title, 'Test Video'); - expect(msg.notificationMetadata!.artist, 'Test Artist'); - expect(msg.notificationMetadata!.album, 'Test Album'); - expect(msg.notificationMetadata!.durationMs, 300000); - expect(msg.notificationMetadata!.artUri, 'https://example.com/art.jpg'); - }); - }); }); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart index f109a6add075..1f1b6d66bec1 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart @@ -223,13 +223,4 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - - @override - _i4.Future setBackgroundPlayback(_i2.BackgroundPlaybackMessage? msg) => - (super.noSuchMethod( - Invocation.method(#setBackgroundPlayback, [msg]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) - as _i4.Future); } diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 0c6431e072d7..8bb9aa23d1ef 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -129,24 +129,6 @@ abstract class VideoPlayerPlatform extends PlatformInterface { throw UnimplementedError('setWebOptions() has not been implemented.'); } - /// Configures background playback and media notification. - /// - /// When [enableBackground] is true, video audio will continue playing when - /// the app is backgrounded. - /// - /// When [notificationMetadata] is provided, a system media notification will - /// be shown with playback controls and the provided metadata (title, artist, etc). - /// If [notificationMetadata] is null, no notification will be shown. - Future setBackgroundPlayback( - int playerId, { - required bool enableBackground, - NotificationMetadata? notificationMetadata, - }) { - throw UnimplementedError( - 'setBackgroundPlayback() has not been implemented.', - ); - } - /// Gets the available audio tracks for the video. Future> getAudioTracks(int playerId) { throw UnimplementedError('getAudioTracks() has not been implemented.'); @@ -171,22 +153,6 @@ abstract class VideoPlayerPlatform extends PlatformInterface { bool isAudioTrackSupportAvailable() { return false; } - - /// Returns whether background playback with media notifications is supported - /// on this platform. - /// - /// This method allows developers to query at runtime whether the current - /// platform supports background playback with system media notifications. - /// This is useful for platforms like web where background playback may not - /// be available. - /// - /// Returns `true` if [setBackgroundPlayback] is supported, `false` otherwise. - /// - /// The default implementation returns `false`. Platform implementations - /// should override this to return `true` if they support background playback. - bool isBackgroundPlaybackSupportAvailable() { - return false; - } } class _PlaceholderImplementation extends VideoPlayerPlatform {} @@ -548,13 +514,13 @@ class VideoPlayerOptions { /// Metadata for the system media notification. /// - /// When provided, a system notification will be shown with media controls - /// (play/pause, seek) and the provided metadata (title, artist, album art, etc.). - /// - /// Requires [allowBackgroundPlayback] to be true for background playback. + /// When provided along with [allowBackgroundPlayback], a system notification + /// will be shown with media controls (play/pause, seek) and the provided + /// metadata (title, artist, album art, etc.). /// /// Note: On Android, this requires setting up foreground service permissions. /// On iOS, this requires enabling background audio capability. + /// This feature is not available on web. final NotificationMetadata? notificationMetadata; /// Additional web controls @@ -661,6 +627,8 @@ class VideoCreationOptions { const VideoCreationOptions({ required this.dataSource, required this.viewType, + this.allowBackgroundPlayback = false, + this.notificationMetadata, }); /// The data source used to create the player. @@ -668,6 +636,17 @@ class VideoCreationOptions { /// The type of view to be used for displaying the video player final VideoViewType viewType; + + /// Whether to allow background playback. + /// + /// When true, video audio will continue playing when the app is backgrounded. + final bool allowBackgroundPlayback; + + /// Metadata for the system media notification. + /// + /// When provided along with [allowBackgroundPlayback], a system notification + /// will be shown with media controls and the provided metadata. + final NotificationMetadata? notificationMetadata; } /// Represents an audio track in a video with its metadata. diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart index 0b7e818b6708..e791b77e422a 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -41,23 +41,6 @@ void main() { expect(initialInstance.isAudioTrackSupportAvailable(), false); }); - test( - 'default implementation setBackgroundPlayback throws unimplemented', - () async { - await expectLater( - () => initialInstance.setBackgroundPlayback(1, enableBackground: true), - throwsUnimplementedError, - ); - }, - ); - - test( - 'default implementation isBackgroundPlaybackSupportAvailable returns false', - () { - expect(initialInstance.isBackgroundPlaybackSupportAvailable(), false); - }, - ); - group('NotificationMetadata', () { test('constructs with required id', () { const metadata = NotificationMetadata(id: 'test_id'); diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index e530925eee89..2b77898c37a8 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -21,4 +21,4 @@ dev_dependencies: # FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. # See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins dependency_overrides: - video_player_platform_interface: {path: ../../video_player_platform_interface} + video_player_platform_interface: {path: ../../../../packages/video_player/video_player_platform_interface} diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 06c12055bfc3..ecc8e427d2da 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -169,16 +169,4 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { /// Sets the audio mode to mix with other sources (ignored). @override Future setMixWithOthers(bool mixWithOthers) => Future.value(); - - /// Returns false as background playback is not supported on web. - @override - bool isBackgroundPlaybackSupportAvailable() => false; - - /// Background playback is not supported on web (silently ignored). - @override - Future setBackgroundPlayback( - int playerId, { - required bool enableBackground, - NotificationMetadata? notificationMetadata, - }) => Future.value(); } From 1fe33837d32cc134e3b9a929d1854ab1ba5bc66b Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Wed, 28 Jan 2026 11:20:28 +0200 Subject: [PATCH 5/9] [video_player_avfoundation] Fix empty notification when no metadata provided Only set up remote command center when notificationMetadata is provided. This prevents iOS from showing an empty/broken media notification when allowBackgroundPlayback is true but no metadata is specified. --- .../Sources/video_player_avfoundation/FVPVideoPlayer.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 1ea5a8d2a7ce..96e884d7cb04 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -557,13 +557,14 @@ - (void)configureBackgroundPlayback:(FVPBackgroundPlaybackMessage *)msg { AVPlayerAudiovisualBackgroundPlaybackPolicyContinuesIfPossible; } - // Set up remote command center for media controls - [self setupRemoteCommandCenter]; - - // Update Now Playing info if metadata is provided + // Only set up remote commands and Now Playing if metadata is provided if (_notificationMetadata) { + [self setupRemoteCommandCenter]; [self updateNowPlayingInfo]; [self setupTimeObserver]; + } else { + // Ensure no stale notification from previous player + [self clearNowPlayingInfo]; } } else { // Disable background playback From d3093c5dcd233d927dba949796f80b0a8229edff Mon Sep 17 00:00:00 2001 From: Igor K Date: Tue, 3 Feb 2026 05:40:35 +0300 Subject: [PATCH 6/9] [video_player] remove unnecessary `MediaBrowserService` action --- .../video_player_android/android/src/main/AndroidManifest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml index 753ea38fce59..368b6125490e 100644 --- a/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml @@ -11,7 +11,6 @@ android:exported="true"> - From e317408f191b1bd086e26d0ffa095a380ebbb30d Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Tue, 10 Feb 2026 13:40:15 +0200 Subject: [PATCH 7/9] [video_player] fix: sync CarPlay Now Playing play/pause button with app playback state --- .../video_player_avfoundation/CHANGELOG.md | 4 +++ .../FVPVideoPlayer.m | 30 +++++++++++++++++++ .../video_player_avfoundation/pubspec.yaml | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 4a6f532fb53c..6bec31391b95 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.10.1 + +* Fixes CarPlay Now Playing play/pause button not updating when playback is controlled from the Flutter app. + ## 2.10.0 * Adds background playback with system media notification support. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 96e884d7cb04..6992dc25fe23 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -420,11 +420,21 @@ - (void)reportInitialized { - (void)playWithError:(FlutterError *_Nullable *_Nonnull)error { _isPlaying = YES; [self updatePlayingState]; +#if TARGET_OS_IOS + if (_enableBackgroundPlayback) { + [self updateNowPlayingPlaybackState]; + } +#endif } - (void)pauseWithError:(FlutterError *_Nullable *_Nonnull)error { _isPlaying = NO; [self updatePlayingState]; +#if TARGET_OS_IOS + if (_enableBackgroundPlayback) { + [self updateNowPlayingPlaybackState]; + } +#endif } - (nullable NSNumber *)position:(FlutterError *_Nullable *_Nonnull)error { @@ -774,6 +784,12 @@ - (void)updateNowPlayingInfo { } [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo; + + // Set initial playbackState for CarPlay CPNowPlayingTemplate. + if (@available(iOS 14.0, *)) { + [MPNowPlayingInfoCenter defaultCenter].playbackState = + _isPlaying ? MPNowPlayingPlaybackStatePlaying : MPNowPlayingPlaybackStatePaused; + } } - (void)updateNowPlayingPlaybackState { @@ -793,10 +809,24 @@ - (void)updateNowPlayingPlaybackState { nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(_isPlaying ? _player.rate : 0.0); [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo; + + // Update playbackState explicitly for CarPlay CPNowPlayingTemplate. + // The lock screen / Control Center infers play/pause from playbackRate in the + // nowPlayingInfo dictionary, but CarPlay's CPNowPlayingTemplate reads the + // playbackState property instead. Although Apple documents this as macOS-only, + // it works on iOS 14+ and is required for CarPlay to reflect the correct + // play/pause button state. + if (@available(iOS 14.0, *)) { + [MPNowPlayingInfoCenter defaultCenter].playbackState = + _isPlaying ? MPNowPlayingPlaybackStatePlaying : MPNowPlayingPlaybackStatePaused; + } } - (void)clearNowPlayingInfo { [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil; + if (@available(iOS 14.0, *)) { + [MPNowPlayingInfoCenter defaultCenter].playbackState = MPNowPlayingPlaybackStateStopped; + } } - (void)setupTimeObserver { diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 815b8248b918..22dfad398aa7 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.10.0 +version: 2.10.1 environment: sdk: ^3.10.0 From 5ba57a617b1a10c1dfd0b016ce6ee7dfeed6cd29 Mon Sep 17 00:00:00 2001 From: Denis Altukhov Date: Thu, 19 Feb 2026 13:58:08 +0200 Subject: [PATCH 8/9] [video_player_avfoundation] Fix registrar property references after rebase Replace self.registrar.messenger with self.binaryMessenger and self.registrar.textures with self.textureRegistry to match the upstream refactor that removed the registrar property from FVPVideoPlayerPlugin. Co-authored-by: Cursor --- .../Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index 422bbcccca13..d471472544ac 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -146,7 +146,7 @@ - (int64_t)configurePlayer:(FVPVideoPlayer *)player [player configureBackgroundPlayback:backgroundPlayback]; } - NSObject *messenger = self.registrar.messenger; + NSObject *messenger = self.binaryMessenger; NSString *channelSuffix = [NSString stringWithFormat:@"%lld", playerIdentifier]; // Set up the player-specific API handler, and its onDispose unregistration. SetUpFVPVideoPlayerInstanceApiWithSuffix(messenger, player, channelSuffix); @@ -263,7 +263,7 @@ - (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions: __weak typeof(self) weakSelf = self; int64_t playerIdentifier = [self configurePlayer:player withExtraDisposeHandler:^() { - [weakSelf.registrar.textures unregisterTexture:textureIdentifier]; + [weakSelf.textureRegistry unregisterTexture:textureIdentifier]; } backgroundPlayback:options.backgroundPlayback]; return [FVPTexturePlayerIds makeWithPlayerId:playerIdentifier textureId:textureIdentifier]; From 425c3e2ee80b5f0849504f61b89e19e7c1a9f31b Mon Sep 17 00:00:00 2001 From: Igor K Date: Tue, 10 Mar 2026 05:36:14 +0300 Subject: [PATCH 9/9] [video_player] fix icons crash --- .../VideoMedia3SessionService.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java index c4dceb75d87a..d0858f443bd3 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoMedia3SessionService.java @@ -487,20 +487,22 @@ public MediaSession.ConnectionResult onConnect( resultBuilder.setAvailablePlayerCommands(availableCommands); // Configure media button layout for notification - ImmutableList customLayout = - ImmutableList.of( - new CommandButton.Builder() - .setPlayerCommand(Player.COMMAND_SEEK_BACK) - .setDisplayName("Rewind") - .build(), - new CommandButton.Builder() - .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) - .setDisplayName("Play/Pause") - .build(), - new CommandButton.Builder() - .setPlayerCommand(Player.COMMAND_SEEK_FORWARD) - .setDisplayName("Fast Forward") - .build()); + ImmutableList customLayout = ImmutableList.of( + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_SEEK_BACK) + .setDisplayName("Rewind") + .setIconResId(android.R.drawable.ic_media_rew) + .build(), + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setDisplayName("Play/Pause") + .setIconResId(android.R.drawable.ic_media_play) + .build(), + new CommandButton.Builder() + .setPlayerCommand(Player.COMMAND_SEEK_FORWARD) + .setDisplayName("Fast Forward") + .setIconResId(android.R.drawable.ic_media_ff) + .build()); resultBuilder.setMediaButtonPreferences(customLayout); }