diff --git a/just_audio/CHANGELOG.md b/just_audio/CHANGELOG.md index 82fee2b7f..394865daf 100644 --- a/just_audio/CHANGELOG.md +++ b/just_audio/CHANGELOG.md @@ -1,6 +1,36 @@ -## 0.10.1 +## 0.9.40 -* +* Fix JDK 21 compile error. + +## 0.9.39 + +* Apply preferPreciseDurationAndTiming to files (@canxin121). +* Add tag parameter to setUrl/setFilePath/setAsset (@mathisfouques). +* Add tag parameter to setClip (@goviral-ma). +* Support rxdart 0.28.x. + +## 0.9.38 + +* Migrate to package:web. +* Add AudioPlayer.setWebCrossOrigin for CORS on web (@danielwinkler). + +## 0.9.37 + +* Support useLazyPreparation on iOS/macOS. +* Add index in sequence to errors for Android/iOS/macOS. +* Fix seek to index UI update on iOS/macOS. + +## 0.9.36 + +* Add setAllowsExternalPlayback on iOS/macOS. +* Support index-based seeking on Android/iOS/macOS. +* Add option to send headers/userAgent without proxy. +* Fix bug where user supplied headers are overwritten by defaults (@ctedgar). + +## 0.9.35 + +* Fix nullable completer argument type (@srawlins). +* Support uuid 4.0.0 (@Pante). ## 0.9.34 diff --git a/just_audio/README.md b/just_audio/README.md index 6c1618f4c..a33b08ec1 100644 --- a/just_audio/README.md +++ b/just_audio/README.md @@ -98,6 +98,7 @@ await playlist.removeAt(3); // Setting the HTTP user agent final player = AudioPlayer( userAgent: 'myradioapp/1.0 (Linux;Android 11) https://myradioapp.com', + useProxyForRequestHeaders: true, // default ); // Setting request headers @@ -105,7 +106,9 @@ final duration = await player.setUrl('https://foo.com/bar.mp3', headers: {'header1': 'value1', 'header2': 'value2'}); ``` -Note: headers are implemented via a local HTTP proxy which on Android, iOS and macOS requires non-HTTPS support to be enabled. See [Platform Specific Configuration](#platform-specific-configuration). +Note: By default, headers are implemented via a local HTTP proxy which on Android, iOS and macOS requires non-HTTPS support to be enabled. See [Platform Specific Configuration](#platform-specific-configuration). + +Alternatively, settings `useProxyForRequestHeaders: false` will use the platform's native headers implementation without a proxy. Although note that iOS doesn't offer an official native API for setting headers, and so this will use the undocumented `AVURLAssetHTTPHeaderFieldsKey` API (or in the case of the user-agent header on iOS 16 and above, the official `AVURLAssetHTTPUserAgentKey` API). ### Working with caches @@ -180,15 +183,18 @@ try { // Catching errors during playback (e.g. lost network connection) player.playbackEventStream.listen((event) {}, onError: (Object e, StackTrace st) { - if (e is PlayerException) { + if (e is PlatformException) { print('Error code: ${e.code}'); print('Error message: ${e.message}'); + print('AudioSource index: ${e.details?["index"]}'); } else { print('An error occurred: $e'); } }); ``` +Note: In a future release, the exception type on `playbackEventStream` will change from `PlatformException` to `PlayerException`. + ### Working with state streams See [The state model](#the-state-model) for details. @@ -430,7 +436,7 @@ Please also consider pressing the thumbs up button at the top of [this page](htt | read from file | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | read from asset | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | read from byte stream | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| request headers | ✅ | ✅ | ✅ | | ✅ | ✅ | +| request headers | ✅ | ✅ | ✅ | * | ✅ | ✅ | | DASH | ✅ | | | | ✅ | ✅ | | HLS | ✅ | ✅ | ✅ | | ✅ | ✅ | | ICY metadata | ✅ | ✅ | ✅ | | | | @@ -450,6 +456,9 @@ Please also consider pressing the thumbs up button at the top of [this page](htt | equalizer | ✅ | | | | | ✅ | | volume boost | ✅ | | | | | ✅ | +(*): While request headers cannot be set directly on Web, cookies can be used to send information in the [Cookie header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie). See also `AudioPlayer.setWebCrossOrigin` to allow sending cookies when loading audio files from the same origin or a different origin. + + ## Experimental features | Feature | Android | iOS | macOS | Web | diff --git a/just_audio/android/build.gradle b/just_audio/android/build.gradle index 2194fc675..4d3fcbbbc 100644 --- a/just_audio/android/build.gradle +++ b/just_audio/android/build.gradle @@ -1,6 +1,6 @@ group 'com.ryanheise.just_audio' version '1.0' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] +def args = ["-Xlint:deprecation","-Xlint:unchecked"] buildscript { repositories { @@ -31,10 +31,10 @@ android { if (project.android.hasProperty("namespace")) { namespace 'com.ryanheise.just_audio' } - compileSdkVersion 33 + compileSdk 34 defaultConfig { - minSdkVersion 16 + minSdk 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -46,12 +46,13 @@ android { sourceCompatibility 1.8 targetCompatibility 1.8 } -} -dependencies { - def exoplayer_version = "2.18.7" - implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" - implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" - implementation "com.google.android.exoplayer:exoplayer-hls:$exoplayer_version" - implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:$exoplayer_version" + dependencies { + def exoplayer_version = "2.18.7" + implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" + implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" + implementation "com.google.android.exoplayer:exoplayer-hls:$exoplayer_version" + implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:$exoplayer_version" + } } + diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java index 37cb1d000..5adf351f0 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java @@ -77,8 +77,6 @@ public class AudioPlayer implements MethodCallHandler, Player.Listener, Metadata private long updatePosition; private long updateTime; private long bufferedPosition; - private Long start; - private Long end; private Long seekPos; private long initialPos; private Integer initialIndex; @@ -100,7 +98,6 @@ public class AudioPlayer implements MethodCallHandler, Player.Listener, Metadata private Map pendingPlaybackEvent; private ExoPlayer player; - private DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); private Integer audioSessionId; private MediaSource mediaSource; private Integer currentIndex; @@ -134,7 +131,14 @@ public void run() { } }; - public AudioPlayer(final Context applicationContext, final BinaryMessenger messenger, final String id, Map audioLoadConfiguration, List rawAudioEffects, Boolean offloadSchedulingEnabled) { + public AudioPlayer( + final Context applicationContext, + final BinaryMessenger messenger, + final String id, + Map audioLoadConfiguration, + List rawAudioEffects, + Boolean offloadSchedulingEnabled + ) { this.context = applicationContext; this.rawAudioEffects = rawAudioEffects; this.offloadSchedulingEnabled = offloadSchedulingEnabled != null ? offloadSchedulingEnabled : false; @@ -143,7 +147,6 @@ public AudioPlayer(final Context applicationContext, final BinaryMessenger messe eventChannel = new BetterEventChannel(messenger, "com.ryanheise.just_audio.events." + id); dataEventChannel = new BetterEventChannel(messenger, "com.ryanheise.just_audio.data." + id); processingState = ProcessingState.none; - extractorsFactory.setConstantBitrateSeekingEnabled(true); if (audioLoadConfiguration != null) { Map loadControlMap = (Map)audioLoadConfiguration.get("androidLoadControl"); if (loadControlMap != null) { @@ -380,10 +383,10 @@ public void onPlayerError(PlaybackException error) { Log.e(TAG, "default ExoPlaybackException: " + exoError.getUnexpectedException().getMessage()); } // TODO: send both errorCode and type - sendError(String.valueOf(exoError.type), exoError.getMessage()); + sendError(String.valueOf(exoError.type), exoError.getMessage(), mapOf("index", currentIndex)); } else { Log.e(TAG, "default PlaybackException: " + error.getMessage()); - sendError(String.valueOf(error.errorCode), error.getMessage()); + sendError(String.valueOf(error.errorCode), error.getMessage(), mapOf("index", currentIndex)); } errorCount++; if (player.hasNextMediaItem() && currentIndex != null && errorCount <= 5) { @@ -588,25 +591,44 @@ private MediaSource getAudioSource(final Object json) { return mediaSource; } + private DefaultExtractorsFactory buildExtractorsFactory(Map options) { + DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + boolean constantBitrateSeekingEnabled = true; + boolean constantBitrateSeekingAlwaysEnabled = false; + int mp3Flags = 0; + if (options != null) { + Map androidExtractorOptions = (Map)options.get("androidExtractorOptions"); + if (androidExtractorOptions != null) { + constantBitrateSeekingEnabled = (Boolean)androidExtractorOptions.get("constantBitrateSeekingEnabled"); + constantBitrateSeekingAlwaysEnabled = (Boolean)androidExtractorOptions.get("constantBitrateSeekingAlwaysEnabled"); + mp3Flags = (Integer)androidExtractorOptions.get("mp3Flags"); + } + } + extractorsFactory.setConstantBitrateSeekingEnabled(constantBitrateSeekingEnabled); + extractorsFactory.setConstantBitrateSeekingAlwaysEnabled(constantBitrateSeekingAlwaysEnabled); + extractorsFactory.setMp3ExtractorFlags(mp3Flags); + return extractorsFactory; + } + private MediaSource decodeAudioSource(final Object json) { Map map = (Map)json; String id = (String)map.get("id"); switch ((String)map.get("type")) { case "progressive": - return new ProgressiveMediaSource.Factory(buildDataSourceFactory(), extractorsFactory) + return new ProgressiveMediaSource.Factory(buildDataSourceFactory(mapGet(map, "headers")), buildExtractorsFactory(mapGet(map, "options"))) .createMediaSource(new MediaItem.Builder() .setUri(Uri.parse((String)map.get("uri"))) .setTag(id) .build()); case "dash": - return new DashMediaSource.Factory(buildDataSourceFactory()) + return new DashMediaSource.Factory(buildDataSourceFactory(mapGet(map, "headers"))) .createMediaSource(new MediaItem.Builder() .setUri(Uri.parse((String)map.get("uri"))) .setMimeType(MimeTypes.APPLICATION_MPD) .setTag(id) .build()); case "hls": - return new HlsMediaSource.Factory(buildDataSourceFactory()) + return new HlsMediaSource.Factory(buildDataSourceFactory(mapGet(map, "headers"))) .createMediaSource(new MediaItem.Builder() .setUri(Uri.parse((String)map.get("uri"))) .setMimeType(MimeTypes.APPLICATION_M3U8) @@ -687,11 +709,24 @@ private void clearAudioEffects() { audioEffectsMap.clear(); } - private DataSource.Factory buildDataSourceFactory() { - String userAgent = Util.getUserAgent(context, "just_audio"); - DataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory() + private DataSource.Factory buildDataSourceFactory(Map headers) { + final Map stringHeaders = castToStringMap(headers); + String userAgent = null; + if (stringHeaders != null) { + userAgent = stringHeaders.remove("User-Agent"); + if (userAgent == null) { + userAgent = stringHeaders.remove("user-agent"); + } + } + if (userAgent == null) { + userAgent = Util.getUserAgent(context, "just_audio"); + } + DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory() .setUserAgent(userAgent) .setAllowCrossProtocolRedirects(true); + if (stringHeaders != null && stringHeaders.size() > 0) { + httpDataSourceFactory.setDefaultRequestProperties(stringHeaders); + } return new DefaultDataSource.Factory(context, httpDataSourceFactory); } @@ -871,7 +906,7 @@ private long getCurrentPosition() { } private long getDuration() { - if (processingState == ProcessingState.none || processingState == ProcessingState.loading) { + if (processingState == ProcessingState.none || processingState == ProcessingState.loading || player == null) { return C.TIME_UNSET; } else { return player.getDuration(); @@ -879,12 +914,16 @@ private long getDuration() { } private void sendError(String errorCode, String errorMsg) { + sendError(errorCode, errorMsg, null); + } + + private void sendError(String errorCode, String errorMsg, Object details) { if (prepareResult != null) { - prepareResult.error(errorCode, errorMsg, null); + prepareResult.error(errorCode, errorMsg, details); prepareResult = null; } - eventChannel.error(errorCode, errorMsg, null); + eventChannel.error(errorCode, errorMsg, details); } private String getLowerCaseExtension(Uri uri) { @@ -1036,6 +1075,15 @@ static Map mapOf(Object... args) { return map; } + static Map castToStringMap(Map map) { + if (map == null) return null; + Map map2 = new HashMap<>(); + for (Object key : map.keySet()) { + map2.put((String)key, (String)map.get(key)); + } + return map2; + } + enum ProcessingState { none, loading, diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java index ac039d0f0..916423e97 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java @@ -34,7 +34,17 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { break; } List rawAudioEffects = call.argument("androidAudioEffects"); - players.put(id, new AudioPlayer(applicationContext, messenger, id, call.argument("audioLoadConfiguration"), rawAudioEffects, call.argument("androidOffloadSchedulingEnabled"))); + players.put( + id, + new AudioPlayer( + applicationContext, + messenger, + id, + call.argument("audioLoadConfiguration"), + rawAudioEffects, + call.argument("androidOffloadSchedulingEnabled") + ) + ); result.success(null); break; } diff --git a/just_audio/darwin/Classes/AudioPlayer.m b/just_audio/darwin/Classes/AudioPlayer.m index ad611ccaf..d22719d01 100644 --- a/just_audio/darwin/Classes/AudioPlayer.m +++ b/just_audio/darwin/Classes/AudioPlayer.m @@ -11,6 +11,8 @@ #import #include +#define TREADMILL_SIZE 2 + // TODO: Check for and report invalid state transitions. // TODO: Apply Apple's guidance on seeking: https://developer.apple.com/library/archive/qa/qa1820/_index.html @implementation AudioPlayer { @@ -39,11 +41,13 @@ @implementation AudioPlayer { FlutterResult _playResult; id _timeObserver; BOOL _automaticallyWaitsToMinimizeStalling; + BOOL _allowsExternalPlayback; LoadControl *_loadControl; BOOL _playing; float _speed; float _volume; BOOL _justAdvanced; + BOOL _enqueuedAll; NSDictionary *_icyMetadata; } @@ -81,6 +85,7 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar _loadResult = nil; _playResult = nil; _automaticallyWaitsToMinimizeStalling = YES; + _allowsExternalPlayback = NO; _loadControl = nil; if (loadConfiguration != (id)[NSNull null]) { NSDictionary *map = loadConfiguration[@"darwinLoadControl"]; @@ -101,6 +106,7 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar _speed = 1.0f; _volume = 1.0f; _justAdvanced = NO; + _enqueuedAll = NO; _icyMetadata = @{}; __weak __typeof__(self) weakSelf = self; [_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { @@ -147,6 +153,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([@"setPreferredPeakBitRate" isEqualToString:call.method]) { [self setPreferredPeakBitRate:(NSNumber *)request[@"bitRate"]]; result(@{}); + } else if ([@"setAllowsExternalPlayback" isEqualToString:call.method]) { + [self setAllowsExternalPlayback:(BOOL)([request[@"allowsExternalPlayback"] intValue] == 1)]; + result(@{}); } else if ([@"seek" isEqualToString:call.method]) { CMTime position = request[@"position"] == (id)[NSNull null] ? kCMTimePositiveInfinity : CMTimeMake([request[@"position"] longLongValue], 1000000); [self seek:position index:request[@"index"] completionHandler:^(BOOL finished) { @@ -166,8 +175,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else { result(FlutterMethodNotImplemented); } - } @catch (id exception) { - //NSLog(@"Error in handleMethodCall"); + } @catch (NSException *exception) { + //NSLog(@"%@", [exception callStackSymbols]); FlutterError *flutterError = [FlutterError errorWithCode:@"error" message:@"Error in handleMethodCall" details:nil]; result(flutterError); } @@ -448,15 +457,16 @@ - (void)metadataOutput:(AVPlayerItemMetadataOutput *)output didOutputTimedMetada - (AudioSource *)decodeAudioSource:(NSDictionary *)data { NSString *type = data[@"type"]; if ([@"progressive" isEqualToString:type]) { - return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl]; + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl headers:data[@"headers"] options:data[@"options"]]; } else if ([@"dash" isEqualToString:type]) { - return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl]; + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl headers:data[@"headers"] options:data[@"options"]]; } else if ([@"hls" isEqualToString:type]) { - return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl]; + return [[UriAudioSource alloc] initWithId:data[@"id"] uri:data[@"uri"] loadControl:_loadControl headers:data[@"headers"] options:data[@"options"]]; } else if ([@"concatenating" isEqualToString:type]) { return [[ConcatenatingAudioSource alloc] initWithId:data[@"id"] audioSources:[self decodeAudioSources:data[@"children"]] - shuffleOrder:(NSArray *)data[@"shuffleOrder"]]; + shuffleOrder:(NSArray *)data[@"shuffleOrder"] + lazyLoading:(NSNumber *)data[@"useLazyPreparation"]]; } else if ([@"clipping" isEqualToString:type]) { return [[ClippingAudioSource alloc] initWithId:data[@"id"] audioSource:(UriAudioSource *)[self decodeAudioSource:data[@"child"]] @@ -514,12 +524,19 @@ - (void)enqueueFrom:(int)index { /* [self dumpQueue]; */ // Regenerate queue + _enqueuedAll = NO; if (!existingItem || _loopMode != loopOne) { + _enqueuedAll = YES; BOOL include = NO; for (int i = 0; i < [_order count]; i++) { int si = [_order[i] intValue]; if (si == _index) include = YES; if (include && _indexedAudioSources[si].playerItem != existingItem) { + if (_indexedAudioSources[si].lazyLoading && _player.items.count >= TREADMILL_SIZE) { + // Enqueue up until the first lazy item that does not fit on the treadmill. + _enqueuedAll = NO; + break; + } //NSLog(@"inserting item %d", si); [_player insertItem:_indexedAudioSources[si].playerItem afterItem:nil]; if (_loopMode == loopOne) { @@ -532,7 +549,7 @@ - (void)enqueueFrom:(int)index { // Add next loop item if we're looping if (_order.count > 0) { - if (_loopMode == loopAll) { + if (_loopMode == loopAll && _enqueuedAll) { int si = [_order[0] intValue]; //NSLog(@"### add loop item:%d", si); if (!_indexedAudioSources[si].playerItem2) { @@ -642,6 +659,9 @@ - (void)load:(NSDictionary *)source initialPosition:(CMTime)initialPosition init options:NSKeyValueObservingOptionNew context:nil]; } + if (@available(macOS 10.11, iOS 6.0, *)) { + _player.allowsExternalPlayback = _allowsExternalPlayback; + } [_player addObserver:self forKeyPath:@"currentItem" options:NSKeyValueObservingOptionNew @@ -938,9 +958,13 @@ - (void)observeValueForKeyPath:(NSString *)keyPath if (_index == [_order[0] intValue] && playerItem == audioSource.playerItem2) { [audioSource flip]; [self enqueueFrom:_index]; + } else if (!_enqueuedAll) { + [self enqueueFrom:_index]; } else { [self updateEndAction]; } + } else if (!_enqueuedAll) { + [self enqueueFrom:_index]; } _justAdvanced = NO; } @@ -958,7 +982,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath - (void)sendErrorForItem:(IndexedPlayerItem *)playerItem { FlutterError *flutterError = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", (int)playerItem.error.code] message:playerItem.error.localizedDescription - details:nil]; + details:@{@"index": @([self indexForItem:playerItem])}]; [self sendError:flutterError playerItem:playerItem]; } @@ -1157,6 +1181,15 @@ - (void)setPreferredPeakBitRate:(NSNumber *)preferredPeakBitRate { } } +- (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { + _allowsExternalPlayback = allowsExternalPlayback; + if (@available(macOS 10.11, iOS 6.0, *)) { + if (_player) { + _player.allowsExternalPlayback = allowsExternalPlayback; + } + } +} + - (void)seek:(CMTime)position index:(NSNumber *)newIndex completionHandler:(void (^)(BOOL))completionHandler { if (_processingState == none || _processingState == loading) { if (completionHandler) { @@ -1238,6 +1271,7 @@ - (void)seek:(CMTime)position index:(NSNumber *)newIndex completionHandler:(void _player.rate = _speed; } } + [self broadcastPlaybackEvent]; completionHandler(YES); } } diff --git a/just_audio/darwin/Classes/AudioSource.m b/just_audio/darwin/Classes/AudioSource.m index 899055754..294215989 100644 --- a/just_audio/darwin/Classes/AudioSource.m +++ b/just_audio/darwin/Classes/AudioSource.m @@ -16,6 +16,13 @@ - (NSString *)sourceId { return _sourceId; } +- (BOOL)lazyLoading { + return NO; +} + +- (void)setLazyLoading:(BOOL)lazyLoading { +} + - (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { return 0; } diff --git a/just_audio/darwin/Classes/ClippingAudioSource.m b/just_audio/darwin/Classes/ClippingAudioSource.m index f976925f3..3c14d1309 100644 --- a/just_audio/darwin/Classes/ClippingAudioSource.m +++ b/just_audio/darwin/Classes/ClippingAudioSource.m @@ -23,6 +23,14 @@ - (UriAudioSource *)audioSource { return _audioSource; } +- (BOOL)lazyLoading { + return _audioSource.lazyLoading; +} + +- (void)setLazyLoading:(BOOL)lazyLoading { + _audioSource.lazyLoading = lazyLoading; +} + - (void)findById:(NSString *)sourceId matches:(NSMutableArray *)matches { [super findById:sourceId matches:matches]; [_audioSource findById:sourceId matches:matches]; diff --git a/just_audio/darwin/Classes/ConcatenatingAudioSource.m b/just_audio/darwin/Classes/ConcatenatingAudioSource.m index 5385c7be3..5ace6af1c 100644 --- a/just_audio/darwin/Classes/ConcatenatingAudioSource.m +++ b/just_audio/darwin/Classes/ConcatenatingAudioSource.m @@ -8,11 +8,12 @@ @implementation ConcatenatingAudioSource { NSArray *_shuffleOrder; } -- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources shuffleOrder:(NSArray *)shuffleOrder { +- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources shuffleOrder:(NSArray *)shuffleOrder lazyLoading:(NSNumber *)lazyLoading { self = [super initWithId:sid]; NSAssert(self, @"super init cannot be nil"); _audioSources = audioSources; _shuffleOrder = shuffleOrder; + self.lazyLoading = [lazyLoading boolValue]; return self; } @@ -20,6 +21,16 @@ - (int)count { return (int)_audioSources.count; } +- (BOOL)lazyLoading { + return [_audioSources count] > 0 ? _audioSources[0].lazyLoading : NO; +} + +- (void)setLazyLoading:(BOOL)lazyLoading { + for (int i = 0; i < [_audioSources count]; i++) { + _audioSources[i].lazyLoading = lazyLoading; + } +} + - (void)insertSource:(AudioSource *)audioSource atIndex:(int)index { [_audioSources insertObject:audioSource atIndex:index]; } diff --git a/just_audio/darwin/Classes/IndexedAudioSource.m b/just_audio/darwin/Classes/IndexedAudioSource.m index 219d31043..94a3b2ff3 100644 --- a/just_audio/darwin/Classes/IndexedAudioSource.m +++ b/just_audio/darwin/Classes/IndexedAudioSource.m @@ -4,6 +4,7 @@ @implementation IndexedAudioSource { BOOL _isAttached; + BOOL _lazyLoading; CMTime _queuedSeekPos; void (^_queuedSeekCompletionHandler)(BOOL); } @@ -12,6 +13,7 @@ - (instancetype)initWithId:(NSString *)sid { self = [super initWithId:sid]; NSAssert(self, @"super init cannot be nil"); _isAttached = NO; + _lazyLoading = NO; _queuedSeekPos = kCMTimeInvalid; _queuedSeekCompletionHandler = nil; return self; @@ -41,6 +43,14 @@ - (BOOL)isAttached { return _isAttached; } +- (BOOL)lazyLoading { + return _lazyLoading; +} + +- (void)setLazyLoading:(BOOL)lazyLoading { + _lazyLoading = lazyLoading; +} + - (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { [sequence addObject:self]; return treeIndex + 1; diff --git a/just_audio/darwin/Classes/LoopingAudioSource.m b/just_audio/darwin/Classes/LoopingAudioSource.m index a8bae2ede..6293f42ab 100644 --- a/just_audio/darwin/Classes/LoopingAudioSource.m +++ b/just_audio/darwin/Classes/LoopingAudioSource.m @@ -14,6 +14,16 @@ - (instancetype)initWithId:(NSString *)sid audioSources:(NSArray return self; } +- (BOOL)lazyLoading { + return [_audioSources count] > 0 ? _audioSources[0].lazyLoading : NO; +} + +- (void)setLazyLoading:(BOOL)lazyLoading { + for (int i = 0; i < [_audioSources count]; i++) { + _audioSources[i].lazyLoading = lazyLoading; + } +} + - (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex { for (int i = 0; i < [_audioSources count]; i++) { treeIndex = [_audioSources[i] buildSequence:sequence treeIndex:treeIndex]; diff --git a/just_audio/darwin/Classes/UriAudioSource.m b/just_audio/darwin/Classes/UriAudioSource.m index baccf6c41..73073301d 100644 --- a/just_audio/darwin/Classes/UriAudioSource.m +++ b/just_audio/darwin/Classes/UriAudioSource.m @@ -10,13 +10,17 @@ @implementation UriAudioSource { IndexedPlayerItem *_playerItem2; /* CMTime _duration; */ LoadControl *_loadControl; + NSMutableDictionary *_headers; + NSDictionary *_options; } -- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri loadControl:(LoadControl *)loadControl { +- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri loadControl:(LoadControl *)loadControl headers:(NSDictionary *)headers options:(NSDictionary *)options { self = [super initWithId:sid]; NSAssert(self, @"super init cannot be nil"); _uri = uri; _loadControl = loadControl; + _headers = headers != (id)[NSNull null] ? [headers mutableCopy] : nil; + _options = options; _playerItem = [self createPlayerItem:uri]; _playerItem2 = nil; return self; @@ -28,10 +32,45 @@ - (NSString *)uri { - (IndexedPlayerItem *)createPlayerItem:(NSString *)uri { IndexedPlayerItem *item; + NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init]; + + if (_options != (id)[NSNull null]) { + NSDictionary *darwinOptions = _options[@"darwinAssetOptions"]; + if (darwinOptions != (id)[NSNull null]) { + assetOptions[AVURLAssetPreferPreciseDurationAndTimingKey] = darwinOptions[@"preferPreciseDurationAndTiming"]; + } + } + if ([uri hasPrefix:@"file://"]) { - item = [[IndexedPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[[uri stringByRemovingPercentEncoding] substringFromIndex:7]]]; + NSURL *fileURL = [NSURL fileURLWithPath:[[uri stringByRemovingPercentEncoding] substringFromIndex:7]]; + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:assetOptions]; + item = [[IndexedPlayerItem alloc] initWithAsset:asset]; } else { - item = [[IndexedPlayerItem alloc] initWithURL:[NSURL URLWithString:uri]]; + if (_headers) { + // Use user-agent key if it is the only header and the API is supported. + if ([_headers count] == 1) { + if (@available(macOS 13.0, iOS 16.0, *)) { + NSString *userAgent = _headers[@"User-Agent"]; + if (userAgent) { + [_headers removeObjectForKey:@"User-Agent"]; + } else { + userAgent = _headers[@"user-agent"]; + if (userAgent) { + [_headers removeObjectForKey:@"user-agent"]; + } + } + if (userAgent) { + assetOptions[AVURLAssetHTTPUserAgentKey] = userAgent; + } + } + } + if ([_headers count] > 0) { + assetOptions[@"AVURLAssetHTTPHeaderFieldsKey"] = _headers; + } + } + + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:uri] options:assetOptions]; + item = [[IndexedPlayerItem alloc] initWithAsset:asset]; } if (@available(macOS 10.13, iOS 11.0, *)) { // This does the best at reducing distortion on voice with speeds below 1.0 diff --git a/just_audio/example/.gitignore b/just_audio/example/.gitignore index 7c36bfa76..3a0d06793 100644 --- a/just_audio/example/.gitignore +++ b/just_audio/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/just_audio/example/android/app/build.gradle b/just_audio/example/android/app/build.gradle index b786c27cd..73918b483 100644 --- a/just_audio/example/android/app/build.gradle +++ b/just_audio/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { namespace 'com.ryanheise.just_audio_example' - compileSdkVersion 33 + compileSdk 34 lintOptions { disable 'InvalidPackage' @@ -35,8 +35,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.ryanheise.just_audio_example" - minSdkVersion 19 - targetSdkVersion 31 + minSdk flutter.minSdkVersion + targetSdk 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/just_audio/example/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/just_audio/example/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java deleted file mode 100644 index 752fc185d..000000000 --- a/just_audio/example/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java +++ /dev/null @@ -1,25 +0,0 @@ -// Generated file. -// -// If you wish to remove Flutter's multidex support, delete this entire file. -// -// Modifications to this file should be done in a copy under a different name -// as this file may be regenerated. - -package io.flutter.app; - -import android.app.Application; -import android.content.Context; -import androidx.annotation.CallSuper; -import androidx.multidex.MultiDex; - -/** - * Extension of {@link android.app.Application}, adding multidex support. - */ -public class FlutterMultiDexApplication extends Application { - @Override - @CallSuper - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - MultiDex.install(this); - } -} diff --git a/just_audio/example/android/build.gradle b/just_audio/example/android/build.gradle index 53a69d400..6bb781444 100644 --- a/just_audio/example/android/build.gradle +++ b/just_audio/example/android/build.gradle @@ -14,13 +14,6 @@ allprojects { google() mavenCentral() } - - gradle.projectsEvaluated{ - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:deprecation" - options.compilerArgs << "-Xlint:unchecked" - } - } } rootProject.buildDir = '../build' @@ -34,3 +27,11 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + +gradle.projectsEvaluated { + project(":just_audio") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Werror" + } + } +} diff --git a/just_audio/example/ios/Flutter/AppFrameworkInfo.plist b/just_audio/example/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d245..8c6e56146 100644 --- a/just_audio/example/ios/Flutter/AppFrameworkInfo.plist +++ b/just_audio/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/just_audio/example/ios/Podfile b/just_audio/example/ios/Podfile index d207307f8..414ba51f1 100644 --- a/just_audio/example/ios/Podfile +++ b/just_audio/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/just_audio/example/ios/Runner.xcodeproj/project.pbxproj b/just_audio/example/ios/Runner.xcodeproj/project.pbxproj index bc869cc2b..fed952262 100644 --- a/just_audio/example/ios/Runner.xcodeproj/project.pbxproj +++ b/just_audio/example/ios/Runner.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E3D27207363A6BEE407ADF2D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -166,7 +167,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -215,6 +216,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -260,6 +262,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E3D27207363A6BEE407ADF2D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -336,7 +356,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -356,8 +376,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -415,7 +438,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -464,7 +487,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -484,8 +507,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -508,8 +534,11 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/just_audio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/just_audio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6e1..e67b2808a 100644 --- a/just_audio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/just_audio/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ with WidgetsBindingObserver { // to the cache file. // await _player.setAudioSource(await _audioSource.resolve()); await _player.setAudioSource(_audioSource); - } catch (e) { + } on PlayerException catch (e) { print("Error loading audio source: $e"); } } diff --git a/just_audio/example/lib/example_effects.dart b/just_audio/example/lib/example_effects.dart index 8ce6ef6d8..c86f1e77b 100644 --- a/just_audio/example/lib/example_effects.dart +++ b/just_audio/example/lib/example_effects.dart @@ -50,7 +50,7 @@ class MyAppState extends State with WidgetsBindingObserver { try { await _player.setAudioSource(AudioSource.uri(Uri.parse( "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"))); - } catch (e) { + } on PlayerException catch (e) { print("Error loading audio source: $e"); } } diff --git a/just_audio/example/lib/example_playlist.dart b/just_audio/example/lib/example_playlist.dart index d752e534a..c0bb9fec6 100644 --- a/just_audio/example/lib/example_playlist.dart +++ b/just_audio/example/lib/example_playlist.dart @@ -96,7 +96,7 @@ class MyAppState extends State with WidgetsBindingObserver { // Preloading audio is not currently supported on Linux. await _player.setAudioSource(_playlist, preload: kIsWeb || defaultTargetPlatform != TargetPlatform.linux); - } catch (e) { + } on PlayerException catch (e) { // Catch load errors: 404, invalid url... print("Error loading audio source: $e"); } diff --git a/just_audio/example/lib/example_radio.dart b/just_audio/example/lib/example_radio.dart index da45fbc90..40456cd15 100644 --- a/just_audio/example/lib/example_radio.dart +++ b/just_audio/example/lib/example_radio.dart @@ -46,7 +46,7 @@ class MyAppState extends State with WidgetsBindingObserver { try { await _player.setAudioSource(AudioSource.uri( Uri.parse("https://stream-uk1.radioparadise.com/aac-320"))); - } catch (e) { + } on PlayerException catch (e) { print("Error loading audio source: $e"); } } diff --git a/just_audio/example/lib/main.dart b/just_audio/example/lib/main.dart index 862a65bc1..2b44956eb 100644 --- a/just_audio/example/lib/main.dart +++ b/just_audio/example/lib/main.dart @@ -46,7 +46,7 @@ class MyAppState extends State with WidgetsBindingObserver { // AAC example: https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.aac await _player.setAudioSource(AudioSource.uri(Uri.parse( "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"))); - } catch (e) { + } on PlayerException catch (e) { print("Error loading audio source: $e"); } } diff --git a/just_audio/example/macos/Runner.xcodeproj/project.pbxproj b/just_audio/example/macos/Runner.xcodeproj/project.pbxproj index baaa740bd..f635c2bd5 100644 --- a/just_audio/example/macos/Runner.xcodeproj/project.pbxproj +++ b/just_audio/example/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/just_audio/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/just_audio/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ad089fa5d..bfbc9cb8e 100644 --- a/just_audio/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/just_audio/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.14.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter audio_session: ^0.1.14 - rxdart: ^0.27.7 + rxdart: ^0.28.0 just_audio_mpv: ^0.1.6 # Other platform implementations below: just_audio: diff --git a/just_audio/ios/Classes/AudioPlayer.h b/just_audio/ios/Classes/AudioPlayer.h index 7a278dbf4..f6a0d7bc6 100644 --- a/just_audio/ios/Classes/AudioPlayer.h +++ b/just_audio/ios/Classes/AudioPlayer.h @@ -12,15 +12,15 @@ @end enum ProcessingState { - none, - loading, - buffering, - ready, - completed + none, + loading, + buffering, + ready, + completed }; enum LoopMode { - loopOff, - loopOne, - loopAll + loopOff, + loopOne, + loopAll }; diff --git a/just_audio/ios/Classes/AudioSource.h b/just_audio/ios/Classes/AudioSource.h index 33641c614..8f7210832 100644 --- a/just_audio/ios/Classes/AudioSource.h +++ b/just_audio/ios/Classes/AudioSource.h @@ -3,6 +3,7 @@ @interface AudioSource : NSObject @property (readonly, nonatomic) NSString* sourceId; +@property (readwrite, nonatomic) BOOL lazyLoading; - (instancetype)initWithId:(NSString *)sid; - (int)buildSequence:(NSMutableArray *)sequence treeIndex:(int)treeIndex; diff --git a/just_audio/ios/Classes/ConcatenatingAudioSource.h b/just_audio/ios/Classes/ConcatenatingAudioSource.h index 9d9bf44a3..e1ded5796 100644 --- a/just_audio/ios/Classes/ConcatenatingAudioSource.h +++ b/just_audio/ios/Classes/ConcatenatingAudioSource.h @@ -5,7 +5,7 @@ @property (readonly, nonatomic) int count; -- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources shuffleOrder:(NSArray *)shuffleOrder; +- (instancetype)initWithId:(NSString *)sid audioSources:(NSMutableArray *)audioSources shuffleOrder:(NSArray *)shuffleOrder lazyLoading:(NSNumber *)lazyLoading; - (void)insertSource:(AudioSource *)audioSource atIndex:(int)index; - (void)removeSourcesFromIndex:(int)start toIndex:(int)end; - (void)moveSourceFromIndex:(int)currentIndex toIndex:(int)newIndex; diff --git a/just_audio/ios/Classes/UriAudioSource.h b/just_audio/ios/Classes/UriAudioSource.h index cd8ac49fc..1e4f92a6f 100644 --- a/just_audio/ios/Classes/UriAudioSource.h +++ b/just_audio/ios/Classes/UriAudioSource.h @@ -6,6 +6,6 @@ @property (readonly, nonatomic) NSString *uri; -- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri loadControl:(LoadControl *)loadControl; +- (instancetype)initWithId:(NSString *)sid uri:(NSString *)uri loadControl:(LoadControl *)loadControl headers:(NSDictionary *)headers options:(NSDictionary *)options; @end diff --git a/just_audio/lib/just_audio.dart b/just_audio/lib/just_audio.dart index b35958241..591fcf781 100644 --- a/just_audio/lib/just_audio.dart +++ b/just_audio/lib/just_audio.dart @@ -58,6 +58,9 @@ class AudioPlayer { /// The user agent to set on all HTTP requests. final String? _userAgent; + /// Whether to use the proxy server to send request headers. + final bool _useProxyForRequestHeaders; + final AudioLoadConfiguration? _audioLoadConfiguration; final bool _androidOffloadSchedulingEnabled; @@ -131,9 +134,11 @@ class AudioPlayer { bool _automaticallyWaitsToMinimizeStalling = true; bool _canUseNetworkResourcesForLiveStreamingWhilePaused = false; double _preferredPeakBitRate = 0; + bool _allowsExternalPlayback = false; bool _playInterrupted = false; bool _platformLoading = false; AndroidAudioAttributes? _androidAudioAttributes; + WebCrossOrigin? _webCrossOrigin; final bool _androidApplyAudioAttributes; final bool _handleAudioSessionActivation; @@ -142,14 +147,22 @@ class AudioPlayer { /// Creates an [AudioPlayer]. /// - /// Apps requesting remote URLs should specify a `[userAgent]` string with - /// this constructor which will be included in the `user-agent` header on all - /// HTTP requests (except on web where the browser's user agent will be sent). - /// This header helps to identify to the server which app is submitting the - /// request. If unspecified, it will default to Apple's Core Audio user agent - /// on iOS/macOS, or just_audio's user agent on Android. Note: this feature - /// is implemented via a local HTTP proxy which requires non-HTTPS support to - /// be enabled. See the README page for setup instructions. + /// Apps requesting remote URLs should set the [userAgent] parameter which + /// will be set as the `user-agent` header on all requests (except on web + /// where the browser's user agent will be used) to identify the client. If + /// unspecified, a platform-specific default will be supplied. + /// + /// Request headers including `user-agent` are sent by default via a local + /// HTTP proxy which requires non-HTTPS support to be enabled (see the README + /// page for setup instructions). Alternatively, you can set + /// [useProxyForRequestHeaders] to `false` to allow supported platforms to + /// send the request headers directly without use of the proxy. On iOS/macOS, + /// this will use the `AVURLAssetHTTPUserAgentKey` on iOS 16 and above, and + /// macOS 13 and above, if `user-agent` is the only header used. Otherwise, + /// the `AVURLAssetHTTPHeaderFieldsKey` key will be used. On Android, this + /// will use ExoPlayer's `setUserAgent` and `setDefaultRequestProperties`. + /// For Linux/Windows federated platform implementations, refer to the + /// documentation for that implementation's support. /// /// The player will automatically pause/duck and resume/unduck when audio /// interruptions occur (e.g. a phone call) or when headphones are unplugged. @@ -172,6 +185,7 @@ class AudioPlayer { AudioLoadConfiguration? audioLoadConfiguration, AudioPipeline? audioPipeline, bool androidOffloadSchedulingEnabled = false, + bool useProxyForRequestHeaders = true, }) : _id = _uuid.v4(), _userAgent = userAgent, _androidApplyAudioAttributes = @@ -179,7 +193,8 @@ class AudioPlayer { _handleAudioSessionActivation = handleAudioSessionActivation, _audioLoadConfiguration = audioLoadConfiguration, _audioPipeline = audioPipeline ?? AudioPipeline(), - _androidOffloadSchedulingEnabled = androidOffloadSchedulingEnabled { + _androidOffloadSchedulingEnabled = androidOffloadSchedulingEnabled, + _useProxyForRequestHeaders = useProxyForRequestHeaders { _audioPipeline._setup(this); if (_audioLoadConfiguration?.darwinLoadControl != null) { _automaticallyWaitsToMinimizeStalling = _audioLoadConfiguration! @@ -550,6 +565,10 @@ class AudioPlayer { /// The preferred peak bit rate (in bits per second) of bandwidth usage on iOS/macOS. double get preferredPeakBitRate => _preferredPeakBitRate; + /// Whether the player allows external playback on iOS/macOS, defaults to + /// false. + bool get allowsExternalPlayback => _allowsExternalPlayback; + /// The current position of the player. Duration get position => _getPositionFor(_playbackEvent); @@ -655,7 +674,7 @@ class AudioPlayer { /// This is equivalent to: /// /// ``` - /// setAudioSource(AudioSource.uri(Uri.parse(url), headers: headers), + /// setAudioSource(AudioSource.uri(Uri.parse(url), headers: headers, tag: tag), /// initialPosition: Duration.zero, preload: true); /// ``` /// @@ -665,9 +684,12 @@ class AudioPlayer { Map? headers, Duration? initialPosition, bool preload = true, + dynamic tag, }) => - setAudioSource(AudioSource.uri(Uri.parse(url), headers: headers), - initialPosition: initialPosition, preload: preload); + setAudioSource( + AudioSource.uri(Uri.parse(url), headers: headers, tag: tag), + initialPosition: initialPosition, + preload: preload); /// Convenience method to set the audio source to a file, preloaded by /// default, with an initial position of zero by default. @@ -675,7 +697,7 @@ class AudioPlayer { /// This is equivalent to: /// /// ``` - /// setAudioSource(AudioSource.uri(Uri.file(filePath)), + /// setAudioSource(AudioSource.uri(Uri.file(filePath), tag: tag), /// initialPosition: Duration.zero, preload: true); /// ``` /// @@ -684,8 +706,9 @@ class AudioPlayer { String filePath, { Duration? initialPosition, bool preload = true, + dynamic tag, }) => - setAudioSource(AudioSource.file(filePath), + setAudioSource(AudioSource.file(filePath, tag: tag), initialPosition: initialPosition, preload: preload); /// Convenience method to set the audio source to an asset, preloaded by @@ -694,7 +717,7 @@ class AudioPlayer { /// For assets within the same package, this is equivalent to: /// /// ``` - /// setAudioSource(AudioSource.uri(Uri.parse('asset:///$assetPath')), + /// setAudioSource(AudioSource.uri(Uri.parse('asset:///$assetPath'), tag: tag), /// initialPosition: Duration.zero, preload: true); /// ``` /// @@ -707,9 +730,10 @@ class AudioPlayer { String? package, bool preload = true, Duration? initialPosition, + dynamic tag, }) => setAudioSource( - AudioSource.asset(assetPath, package: package), + AudioSource.asset(assetPath, package: package, tag: tag), initialPosition: initialPosition, preload: preload, ); @@ -848,7 +872,8 @@ class AudioPlayer { return duration; } on PlatformException catch (e) { try { - throw PlayerException(int.parse(e.code), e.message); + throw PlayerException(int.parse(e.code), e.message, + (e.details as Map?)?.cast()); } on FormatException catch (_) { if (e.code == 'abort') { throw PlayerInterruptedException(e.message); @@ -864,7 +889,8 @@ class AudioPlayer { /// original [AudioSource]. If [end] is null, it will be reset to the end of /// the original [AudioSource]. This method cannot be called from the /// [ProcessingState.idle] state. - Future setClip({Duration? start, Duration? end}) async { + Future setClip( + {Duration? start, Duration? end, dynamic tag}) async { if (_disposed) return null; _setPlatformActive(true)?.catchError((dynamic e) async => null); final duration = await _load( @@ -875,6 +901,7 @@ class AudioPlayer { child: _audioSource as UriAudioSource, start: start, end: end, + tag: tag, )); return duration; } @@ -1100,6 +1127,16 @@ class AudioPlayer { SetPreferredPeakBitRateRequest(bitRate: preferredPeakBitRate)); } + /// Sets allowsExternalPlayback on iOS/macOS, defaults to false. + Future setAllowsExternalPlayback( + final bool allowsExternalPlayback) async { + if (_disposed) return; + _allowsExternalPlayback = allowsExternalPlayback; + await (await _platform).setAllowsExternalPlayback( + SetAllowsExternalPlaybackRequest( + allowsExternalPlayback: allowsExternalPlayback)); + } + /// Seeks to a particular [position]. If a composition of multiple /// [AudioSource]s has been loaded, you may also specify [index] to seek to a /// particular item within that sequence. This method has no effect unless @@ -1167,6 +1204,29 @@ class AudioPlayer { usage: audioAttributes.usage.value)); } + /// Set the `crossorigin` attribute on the `