diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 4e4c13b0a0f2..b19677919bdd 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.12.0 +* Forwards `VideoPlayerOptions.preferredAudioLanguage` during player creation so platform implementations can apply the preferred audio language per player instance. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.11.1 diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 3453583bf351..45be958d2a49 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -26,6 +26,13 @@ dev_dependencies: path_provider: ^2.0.6 test: any +# 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: ../../../../packages/video_player/video_player_android} + video_player_avfoundation: {path: ../../../../packages/video_player/video_player_avfoundation} + video_player_platform_interface: {path: ../../../../packages/video_player/video_player_platform_interface} + flutter: uses-material-design: true assets: @@ -35,3 +42,4 @@ flutter: - assets/bumble_bee_captions.srt - assets/bumble_bee_captions.vtt - assets/Audio.mp3 + - assets/multi_audio.mp4 diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b287a39fed24..66cc5640c1a5 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -560,6 +560,7 @@ class VideoPlayerController extends ValueNotifier { final creationOptions = platform_interface.VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, + videoPlayerOptions: videoPlayerOptions ); if (videoPlayerOptions?.mixWithOthers != null) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c55f4e823c4e..14f07428f3f9 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, macOS and web. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.11.1 +version: 2.12.0 environment: sdk: ^3.10.0 @@ -39,3 +39,9 @@ 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: ../../../packages/video_player/video_player_android} + video_player_avfoundation: {path: ../../../packages/video_player/video_player_avfoundation} + video_player_platform_interface: {path: ../../../packages/video_player/video_player_platform_interface} 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 56cd402f1228..a47cc52a40f1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1754,6 +1754,22 @@ void main() { expect(controller.videoPlayerOptions!.mixWithOthers, true); }); + test('preferredAudioLanguage is forwarded to createWithOptions', () async { + final controller = VideoPlayerController.networkUrl( + _localhostUri, + videoPlayerOptions: VideoPlayerOptions(preferredAudioLanguage: 'es'), + ); + addTearDown(controller.dispose); + + await controller.initialize(); + + expect(fakeVideoPlayerPlatform.creationOptions.length, 1); + expect( + fakeVideoPlayerPlatform.creationOptions[0].videoPlayerOptions?.preferredAudioLanguage, + 'es', + ); + }); + test('true allowBackgroundPlayback continues playback', () async { final controller = VideoPlayerController.networkUrl( _localhostUri, @@ -1899,6 +1915,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { List calls = []; List dataSources = []; List viewTypes = []; + List creationOptions = []; final Map> streams = >{}; bool forceInitError = false; int nextPlayerId = 0; @@ -1943,6 +1960,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } dataSources.add(options.dataSource); viewTypes.add(options.viewType); + creationOptions.add(options); return nextPlayerId++; } diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 467d0b1dce3b..2de3f7210f7c 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.12.0 + +* Adds support for setting a preferred audio language during player creation via `VideoPlayerOptions.preferredAudioLanguage`. + ## 2.11.0 * Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. 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 5401e1974e2e..77d44b52641b 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 @@ -84,6 +84,7 @@ public VideoPlayer( exoPlayer.prepare(); exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer)); setAudioAttributes(exoPlayer, options.mixWithOthers); + setPreferredAudioLanguage(exoPlayer, options.preferredAudioLanguage); } public void setDisposeHandler(@Nullable DisposeHandler handler) { @@ -447,6 +448,18 @@ public void selectVideoTrack(long groupIndex, long trackIndex) { trackSelector.buildUponParameters().setOverrideForType(override).build()); } + private static void setPreferredAudioLanguage( + ExoPlayer exoPlayer, String preferredAudioLanguage) { + if (preferredAudioLanguage != null) { + exoPlayer.setTrackSelectionParameters( + exoPlayer + .getTrackSelectionParameters() + .buildUpon() + .setPreferredAudioLanguage(preferredAudioLanguage) + .build()); + } + } + public void dispose() { isDisposed = true; mainHandler.removeCallbacksAndMessages(null); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java index 851ed1150d8e..b825caf6e44d 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -9,6 +9,7 @@ public class VideoPlayerOptions { public boolean mixWithOthers; + @Nullable public String preferredAudioLanguage; /** * The duration of the back buffer in milliseconds, used to configure ExoPlayer's load control. @@ -21,5 +22,6 @@ public VideoPlayerOptions() {} public VideoPlayerOptions(@NonNull VideoPlayerOptions other) { this.mixWithOthers = other.mixWithOthers; this.backBufferDurationMs = other.backBufferDurationMs; + this.preferredAudioLanguage = other.preferredAudioLanguage; } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 18cdb992f8fb..af69ec71e7e2 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: ../../../../packages/video_player/video_player_platform_interface} diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 11e0efc18141..a5180a3d4bbf 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.11.0 +version: 2.12.0 environment: sdk: ^3.12.0 @@ -33,3 +33,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: ../../../packages/video_player/video_player_platform_interface} diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index f97f9f1c5e41..7c14b6777799 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.11.1 + +* Adds support for setting a preferred audio language during player creation via `VideoPlayerOptions.preferredAudioLanguage`. + ## 2.11.0 * Implements `setPreventsDisplaySleepDuringVideoPlayback` using diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift index 951f3bcfed67..5c38236c7234 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPlugin.swift @@ -166,6 +166,7 @@ public final class VideoPlayerPlugin: NSObject, FlutterPlugin, AVFoundationVideo func createPlatformViewPlayer(options params: CreationOptions) throws -> Int64 { let item = try playerItem(with: params) let player = FVPVideoPlayer(playerItem: item, avFactory: avFactory, viewProvider: viewProvider) + player.preferredAudioLanguage = params.preferredAudioLanguage return configurePlayer(player, extraDisposeHandler: nil) } @@ -183,6 +184,7 @@ public final class VideoPlayerPlugin: NSObject, FlutterPlugin, AVFoundationVideo avFactory: avFactory, viewProvider: viewProvider ) + player.preferredAudioLanguage = creationOptions.preferredAudioLanguage let textureId = textureRegistry.register(player) player.setTextureIdentifier(textureId) diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift index 3cd23143abc7..fd166d050697 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayerPluginMessages.g.swift @@ -73,7 +73,7 @@ private func doubleEqualsVideoPlayerPluginMessages(_ lhs: Double, _ rhs: Double) private func doubleHashVideoPlayerPluginMessages(_ value: Double, _ hasher: inout Hasher) { if value.isNaN { - hasher.combine(0x7FF8_0000_0000_0000) + hasher.combine(0x7FF8000000000000) } else { // Normalize -0.0 to 0.0 hasher.combine(value == 0 ? 0 : value) @@ -176,12 +176,14 @@ func deepHashVideoPlayerPluginMessages(value: Any?, hasher: inout Hasher) { } } + /// Information passed to the platform view creation. /// /// Generated class from Pigeon that represents data sent in messages. struct PlatformVideoViewCreationParams: Hashable { var playerId: Int64 + // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> PlatformVideoViewCreationParams? { let playerId = pigeonVar_list[0] as! Int64 @@ -195,9 +197,7 @@ struct PlatformVideoViewCreationParams: Hashable { playerId ] } - static func == (lhs: PlatformVideoViewCreationParams, rhs: PlatformVideoViewCreationParams) - -> Bool - { + static func == (lhs: PlatformVideoViewCreationParams, rhs: PlatformVideoViewCreationParams) -> Bool { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } @@ -214,35 +214,40 @@ struct PlatformVideoViewCreationParams: Hashable { struct CreationOptions: Hashable { var uri: String var httpHeaders: [String: String] + var preferredAudioLanguage: String? = nil + // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> CreationOptions? { let uri = pigeonVar_list[0] as! String let httpHeaders = pigeonVar_list[1] as! [String: String] + let preferredAudioLanguage: String? = nilOrValue(pigeonVar_list[2]) return CreationOptions( uri: uri, - httpHeaders: httpHeaders + httpHeaders: httpHeaders, + preferredAudioLanguage: preferredAudioLanguage ) } func toList() -> [Any?] { return [ uri, httpHeaders, + preferredAudioLanguage, ] } static func == (lhs: CreationOptions, rhs: CreationOptions) -> Bool { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsVideoPlayerPluginMessages(lhs.uri, rhs.uri) - && deepEqualsVideoPlayerPluginMessages(lhs.httpHeaders, rhs.httpHeaders) + return deepEqualsVideoPlayerPluginMessages(lhs.uri, rhs.uri) && deepEqualsVideoPlayerPluginMessages(lhs.httpHeaders, rhs.httpHeaders) && deepEqualsVideoPlayerPluginMessages(lhs.preferredAudioLanguage, rhs.preferredAudioLanguage) } func hash(into hasher: inout Hasher) { hasher.combine("CreationOptions") deepHashVideoPlayerPluginMessages(value: uri, hasher: &hasher) deepHashVideoPlayerPluginMessages(value: httpHeaders, hasher: &hasher) + deepHashVideoPlayerPluginMessages(value: preferredAudioLanguage, hasher: &hasher) } } @@ -251,6 +256,7 @@ struct TexturePlayerIds: Hashable { var playerId: Int64 var textureId: Int64 + // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> TexturePlayerIds? { let playerId = pigeonVar_list[0] as! Int64 @@ -271,8 +277,7 @@ struct TexturePlayerIds: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsVideoPlayerPluginMessages(lhs.playerId, rhs.playerId) - && deepEqualsVideoPlayerPluginMessages(lhs.textureId, rhs.textureId) + return deepEqualsVideoPlayerPluginMessages(lhs.playerId, rhs.playerId) && deepEqualsVideoPlayerPluginMessages(lhs.textureId, rhs.textureId) } func hash(into hasher: inout Hasher) { @@ -325,8 +330,7 @@ private class VideoPlayerPluginMessagesPigeonCodecReaderWriter: FlutterStandardR } class VideoPlayerPluginMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = VideoPlayerPluginMessagesPigeonCodec( - readerWriter: VideoPlayerPluginMessagesPigeonCodecReaderWriter()) + static let shared = VideoPlayerPluginMessagesPigeonCodec(readerWriter: VideoPlayerPluginMessagesPigeonCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. @@ -342,15 +346,9 @@ protocol AVFoundationVideoPlayerApi { class AVFoundationVideoPlayerApiSetup { static var codec: FlutterStandardMessageCodec { VideoPlayerPluginMessagesPigeonCodec.shared } /// Sets up an instance of `AVFoundationVideoPlayerApi` to handle messages through the `binaryMessenger`. - static func setUp( - binaryMessenger: FlutterBinaryMessenger, api: AVFoundationVideoPlayerApi?, - messageChannelSuffix: String = "" - ) { + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AVFoundationVideoPlayerApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let initializeChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec) + let initializeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { initializeChannel.setMessageHandler { _, reply in do { @@ -363,10 +361,7 @@ class AVFoundationVideoPlayerApiSetup { } else { initializeChannel.setMessageHandler(nil) } - let createForPlatformViewChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec) + let createForPlatformViewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { createForPlatformViewChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -381,10 +376,7 @@ class AVFoundationVideoPlayerApiSetup { } else { createForPlatformViewChannel.setMessageHandler(nil) } - let createForTextureViewChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec) + let createForTextureViewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { createForTextureViewChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -399,10 +391,7 @@ class AVFoundationVideoPlayerApiSetup { } else { createForTextureViewChannel.setMessageHandler(nil) } - let setMixWithOthersChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec) + let setMixWithOthersChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { setMixWithOthersChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -417,10 +406,7 @@ class AVFoundationVideoPlayerApiSetup { } else { setMixWithOthersChannel.setMessageHandler(nil) } - let getAssetUrlChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec) + let getAssetUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getAssetUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m index 316aa6237c25..757c567a5612 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m @@ -407,10 +407,41 @@ - (void)reportInitialized { NSAssert(!_isInitialized, @"reportInitializedIfReadyToPlay should only be called once."); _isInitialized = YES; + [self setPreferredAudioLanguage]; [self.eventListener videoPlayerDidInitializeWithDuration:self.duration size:currentItem.presentationSize]; } +/// Selects the audio track whose locale language code matches self.preferredAudioLanguage. +/// Does nothing if preferredAudioLanguage is nil, the player is not yet initialized, or no +/// matching track is found. +- (void)setPreferredAudioLanguage { + if (!self.preferredAudioLanguage || !_isInitialized) { + return; + } + + NSString *preferredLanguage = self.preferredAudioLanguage; + + AVPlayerItem *currentItem = _player.currentItem; + if (!currentItem) { + return; + } + + AVMediaSelectionGroup *audioGroup = + [currentItem.asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + if (!audioGroup) { + return; + } + + for (AVMediaSelectionOption *option in audioGroup.options) { + NSString *optionLanguageCode = option.locale.languageCode; + if (optionLanguageCode && [optionLanguageCode isEqualToString:preferredLanguage]) { + [currentItem selectMediaOption:option inMediaSelectionGroup:audioGroup]; + break; + } + } +} + #pragma mark - FVPVideoPlayerInstanceApi - (void)playWithError:(FlutterError *_Nullable *_Nonnull)error { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer.h index be10a3317e97..d56d61fbd6e2 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer.h @@ -29,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) BOOL disposed; /// Indicates whether the video player is set to loop. @property(nonatomic) BOOL isLooping; +/// The preferred audio language code +@property(nonatomic, copy, nullable) NSString *preferredAudioLanguage; /// The current playback position of the video, in milliseconds. @property(nonatomic, readonly) int64_t position; /// The event listener to report video events to. diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 9ebd74df6edd..ac6b98ffaebe 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: ../../../../packages/video_player/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 c98edfc15ec8..a9b160f0ae1e 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 @@ -90,7 +90,11 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { if (uri == null) { throw ArgumentError('Unable to construct a video asset from $options'); } - final pigeonCreationOptions = CreationOptions(uri: uri, httpHeaders: dataSource.httpHeaders); + final pigeonCreationOptions = CreationOptions( + uri: uri, + httpHeaders: dataSource.httpHeaders, + preferredAudioLanguage: options.videoPlayerOptions?.preferredAudioLanguage, + ); final int playerId; final VideoPlayerViewState state; diff --git a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart index cca6f571848d..cd967ffae7ec 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/video_player_plugin_messages.g.dart @@ -13,9 +13,9 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, + List? replyList, + String channelName, { + required bool isNullValid, }) { if (replyList == null) { throw PlatformException( @@ -49,7 +49,8 @@ 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) { if (a.length != b.length) { @@ -98,23 +99,29 @@ int _deepHash(Object? value) { return value.hashCode; } + /// 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 @@ -135,25 +142,35 @@ class PlatformVideoViewCreationParams { } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({ + required this.uri, + required this.httpHeaders, + this.preferredAudioLanguage, + }); String uri; Map httpHeaders; + String? preferredAudioLanguage; + List _toList() { - return [uri, httpHeaders]; + return [ + uri, + httpHeaders, + preferredAudioLanguage, + ]; } 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(), + preferredAudioLanguage: result[2] as String?, ); } @@ -166,7 +183,7 @@ class CreationOptions { if (identical(this, other)) { return true; } - return _deepEquals(uri, other.uri) && _deepEquals(httpHeaders, other.httpHeaders); + return _deepEquals(uri, other.uri) && _deepEquals(httpHeaders, other.httpHeaders) && _deepEquals(preferredAudioLanguage, other.preferredAudioLanguage); } @override @@ -175,23 +192,31 @@ class CreationOptions { } 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; - return TexturePlayerIds(playerId: result[0]! as int, textureId: result[1]! as int); + return TexturePlayerIds( + playerId: result[0]! as int, + textureId: result[1]! as int, + ); } @override @@ -211,6 +236,7 @@ class TexturePlayerIds { int get hashCode => _deepHash([runtimeType, ..._toList()]); } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -218,13 +244,13 @@ 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 { @@ -252,10 +278,8 @@ class AVFoundationVideoPlayerApi { /// 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' - : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -263,8 +287,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, @@ -273,12 +296,16 @@ class AVFoundationVideoPlayerApi { final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } 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, @@ -288,16 +315,16 @@ class AVFoundationVideoPlayerApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; return pigeonVar_replyValue! as int; } Future createForTextureView(CreationOptions creationOptions) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -307,16 +334,16 @@ class AVFoundationVideoPlayerApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; return pigeonVar_replyValue! as TexturePlayerIds; } 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, @@ -325,12 +352,16 @@ class AVFoundationVideoPlayerApi { final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } 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, @@ -340,10 +371,11 @@ class AVFoundationVideoPlayerApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; return pigeonVar_replyValue as String?; } } diff --git a/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart b/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart index 91f4fc80cbda..c670144682df 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/video_player_plugin_messages.dart @@ -22,10 +22,11 @@ class PlatformVideoViewCreationParams { } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({required this.uri, required this.httpHeaders, this.preferredAudioLanguage}); String uri; Map httpHeaders; + String? preferredAudioLanguage; } class TexturePlayerIds { diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 9a1e29eab6d1..08b388b117a5 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.11.0 +version: 2.11.1 environment: sdk: ^3.10.0 @@ -37,3 +37,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: ../../../packages/video_player/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 16dfd7e9d881..d78973be90f4 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 @@ -282,6 +282,68 @@ void main() { ); }); + test('createWithOptions forwards preferredAudioLanguage for texture view', () async { + final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = + setUpMockPlayer(playerId: 1, textureId: 101); + const newPlayerId = 2; + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 102)); + + const preferredAudioLanguage = 'es'; + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource(sourceType: DataSourceType.file, uri: 'file:///foo/bar'), + viewType: VideoViewType.textureView, + videoPlayerOptions: VideoPlayerOptions(preferredAudioLanguage: preferredAudioLanguage), + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.preferredAudioLanguage, preferredAudioLanguage); + }); + + test('createWithOptions forwards preferredAudioLanguage for platform view', () async { + final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = + setUpMockPlayer(playerId: 1); + const newPlayerId = 2; + when(api.createForPlatformView(any)).thenAnswer((_) async => newPlayerId); + + const preferredAudioLanguage = 'fr'; + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource(sourceType: DataSourceType.file, uri: 'file:///foo/bar'), + viewType: VideoViewType.platformView, + videoPlayerOptions: VideoPlayerOptions(preferredAudioLanguage: preferredAudioLanguage), + ), + ); + + final VerificationResult verification = verify(api.createForPlatformView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.preferredAudioLanguage, preferredAudioLanguage); + }); + + test('createWithOptions keeps preferredAudioLanguage null when options are absent', () async { + final (AVFoundationVideoPlayer player, MockAVFoundationVideoPlayerApi api, _) = + setUpMockPlayer(playerId: 1, textureId: 101); + const newPlayerId = 2; + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 102)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource(sourceType: DataSourceType.file, uri: 'file:///foo/bar'), + viewType: VideoViewType.textureView, + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.preferredAudioLanguage, isNull); + }); + test('setLooping', () async { final (AVFoundationVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = setUpMockPlayer(playerId: 1); diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 617d0983b225..a0bd6c53fcad 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.10.0 + +* Adds `videoPlayerOptions` to `VideoCreationOptions` to allow platform implementations to receive per-player options at creation time. + ## 6.9.0 * Adds `backBufferDurationMs` to `VideoPlayerOptions` to support configuring the back buffer duration. 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 ca5edf84739b..20b9f6af6448 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 @@ -473,6 +473,7 @@ class VideoPlayerOptions { this.preventsDisplaySleepDuringVideoPlayback = true, this.webOptions, this.backBufferDurationMs, + this.preferredAudioLanguage, }) : assert( backBufferDurationMs == null || backBufferDurationMs >= 0, 'backBufferDurationMs must be zero or greater', @@ -504,6 +505,9 @@ class VideoPlayerOptions { /// /// Ignored on platforms that do not support controlling the back buffer. final int? backBufferDurationMs; + + /// Preferred audio language for the video. + final String? preferredAudioLanguage; } /// [VideoPlayerWebOptions] can be optionally used to set additional web settings diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 4e113a877e2b..ed867bc9f90d 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.9.0 +version: 6.10.0 environment: sdk: ^3.10.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 91e4d8fdb062..fb528460cf4a 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 @@ -26,4 +26,12 @@ void main() { final options = VideoPlayerOptions(backBufferDurationMs: 20000); expect(options.backBufferDurationMs, 20000); }); + test('VideoPlayerOptions preferredAudioLanguage defaults to null', () { + final options = VideoPlayerOptions(); + expect(options.preferredAudioLanguage, null); + }); + test('VideoPlayerOptions preferredAudioLanguage is set correctly', () { + final options = VideoPlayerOptions(preferredAudioLanguage: 'en'); + expect(options.preferredAudioLanguage, 'en'); + }); } diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 04fc830db44d..d0faeb7048fa 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: ../../../../packages/video_player/video_player_platform_interface} diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index a69ae3c1188c..b104d2e48d7e 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -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: ../../../packages/video_player/video_player_platform_interface}