ryanheise / audio_service

Flutter plugin to play audio in the background while the screen is off.
806 stars 480 forks source link

Audio_Service currentmediaItemStream not working on android null-safety version #621

Closed andy-t-wang closed 3 years ago

andy-t-wang commented 3 years ago

Which API doesn't behave as documented, and how does it misbehave? currentMediaItemStream, will always be null despite a media item being played. No matter what audio Item in the queue is selected it will always be null. Minimal reproduction project child: StreamBuilder<MediaItem?>( stream: AudioService.currentMediaItemStream, builder: (context, snapshot) { final mediaItem = AudioService.currentMediaItem; The audioplayertask code is the same as the example. I'm assuming the stream gets set at the onStart.

To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior: User the null safety version

  1. Initialize the audio_service.
  2. Play something in the queue.
  3. The currentmediaItem stream will always be null on android, works fine on ios.

Error messages

../../sdk/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_cache_manager-3.0.0-nullsafety.1/lib/src/storage/cache_info_repositories/cache_object_provider.dart:208:29: Warning: Operand of null-aware operation '!' has type 'String' which excludes null.
    final oldDbPath = join((await getDatabasesPath())!, '$databaseName.db');
                            ^
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
D/InputTransport(30704): Input channel constructed: fd=94
D/InputMethodManager(30704): prepareNavigationBarInfo() DecorView@f32c62e[MainActivity]
D/InputMethodManager(30704): getNavigationBarColor() -855310
V/InputMethodManager(30704): Starting input: tba=com.example.storio ic=null mNaviBarColor -855310 mIsGetNaviBarColorSuccess true , NavVisible : true , NavTrans : false
D/InputMethodManager(30704): startInputInner - Id : 0
../../sdk/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_cache_manager-3.0.0-nullsafety.1/lib/src/storage/cache_info_repositories/cache_object_provider.dart:208:29: Warning: Operand of null-aware operation '!' has type 'String' which excludes null.
    final oldDbPath = join((await getDatabasesPath())!, '$databaseName.db');
                            ^

Connecting to VM Service at ws://127.0.0.1:58687/ZmS2izbz8I8=/ws
D/MediaBrowserCompat(30704): Connecting to a MediaBrowserService.
I/flutter (30704): true
W/DynamiteModule(30704): Local module descriptor class for providerinstaller not found.
I/DynamiteModule(30704): Considering local module providerinstaller:0 and remote module providerinstaller:0
W/ProviderInstaller(30704): Failed to load providerinstaller module: No acceptable module found. Local version is 0 and remote version is 0.
2
I/.example.stori(30704): The ClassLoaderContext is a special shared library.
D/ConnectivityManager(30704): requestNetwork; CallingUid : 10250, CallingPid : 30704
I/.example.stori(30704): The ClassLoaderContext is a special shared library.
W/.example.stori(30704): Unknown chunk type '200'.
W/.example.stori(30704): Accessing hidden field Ljava/nio/Buffer;->address:J (light greylist, reflection)
V/NativeCrypto(30704): Registering com/google/android/gms/org/conscrypt/NativeCrypto's 287 native methods...
W/.example.stori(30704): Accessing hidden method Ljava/security/spec/ECParameterSpec;->getCurveName()Ljava/lang/String; (light greylist, reflection)
I/ProviderInstaller(30704): Installed default security provider GmsCore_OpenSSL
D/ConnectivityManager(30704): requestNetwork; CallingUid : 10250, CallingPid : 30704
W/.example.stori(30704): Accessing hidden field Ljava/net/Socket;->impl:Ljava/net/SocketImpl; (light greylist, reflection)
W/.example.stori(30704): Accessing hidden method Ldalvik/system/CloseGuard;->get()Ldalvik/system/CloseGuard; (light greylist, linking)
W/.example.stori(30704): Accessing hidden method Ldalvik/system/CloseGuard;->open(Ljava/lang/String;)V (light greylist, linking)
W/.example.stori(30704): Accessing hidden field Ljava/io/FileDescriptor;->descriptor:I (light greylist, JNI)
W/.example.stori(30704): Accessing hidden method Ljava/security/spec/ECParameterSpec;->setCurveName(Ljava/lang/String;)V (light greylist, reflection)
W/.example.stori(30704): Accessing hidden method Ldalvik/system/BlockGuard;->getThreadPolicy()Ldalvik/system/BlockGuard$Policy; (light greylist, linking)
W/.example.stori(30704): Accessing hidden method Ldalvik/system/BlockGuard$Policy;->onNetwork()V (light greylist, linking)
I/flutter (30704): {id: https://firebasestorage.googleapis.com/v0/b/epic-1a104.appspot.com/o/users%2Ffoo@gmail.com%2Fstories%2Fae6afb60-8540-11eb-99ad-b7f65b0f2a01?alt=media&token=0b2ebc57-8386-411f-83c8-dcfa82f28216, album: , title: How the FDA came to be, artist: joe foo, genre: null, duration: 134676, artUri: https://firebasestorage.googleapis.com/v0/b/epic-1a104.appspot.com/o/users%2Ffoo@gmail.com%2Fpicture%2Fprofile_picture?alt=media&token=4676d2ef-4c34-4b2a-90ee-5ddbaede1141, playable: true, displayTitle: null, displaySubtitle: null, displayDescription: From a great big story, rating: null, extras: {date: Mar 14, 2021, email_id: foo@gmail.com, firestore_id: LHwddipub8vT2g7SuYcu}}
W/.example.stori(30704): Accessing hidden method Landroid/media/AudioTrack;->getLatency()I (light greylist, reflection)
I/ExoPlayerImpl(30704): Init 857723a [ExoPlayerLib/2.13.1] [dreamqlteue, SM-G950U1, samsung, 28]
2
I/System.out(30704): (HTTPLog)-Static: isSBSettingEnabled false
W/.example.stori(30704): Accessing hidden method Ldalvik/system/CloseGuard;->close()V (light greylist, linking)
4
I/System.out(30704): (HTTPLog)-Static: isSBSettingEnabled false
W/AudioCapabilities(30704): Unsupported mime audio/ac4
W/AudioCapabilities(30704): Unsupported mime audio/x-ima
W/AudioCapabilities(30704): Unsupported mime audio/eac3-joc
W/AudioCapabilities(30704): Unsupported mime audio/evrc
W/AudioCapabilities(30704): Unsupported mime audio/mpeg-L1
W/AudioCapabilities(30704): Unsupported mime audio/mpeg-L2
W/AudioCapabilities(30704): Unsupported mime audio/qcelp
W/AudioCapabilities(30704): Unsupported mime audio/x-ms-wma
W/AudioCapabilities(30704): Unsupported mime audio/evrc
W/AudioCapabilities(30704): Unsupported mime audio/qcelp
W/VideoCapabilities(30704): Unrecognized profile 4 for video/hevc
W/VideoCapabilities(30704): Unsupported mime video/mp43
W/VideoCapabilities(30704): Unrecognized profile/level 1/32 for video/mp4v-es
W/VideoCapabilities(30704): Unrecognized profile/level 32768/2 for video/mp4v-es
W/VideoCapabilities(30704): Unrecognized profile/level 32768/64 for video/mp4v-es
2
W/VideoCapabilities(30704): Unsupported mime video/wvc1
W/VideoCapabilities(30704): Unsupported mime video/x-ms-wmv7
W/VideoCapabilities(30704): Unsupported mime video/x-ms-wmv8
I/VideoCapabilities(30704): Unsupported profile 4 for video/mp4v-es
I/ACodec  (30704):  [] Now uninitialized
I/ACodec  (30704): [] onAllocateComponent
I/OMXClient(30704): IOmx service obtained
I/ACodec  (30704): [OMX.google.aac.decoder] Now Loaded
I/ACodec  (30704): codec does not support config priority (err -2147483648)
I/ACodec  (30704): [OMX.google.aac.decoder] Now Loaded->Idle
I/ACodec  (30704): [OMX.google.aac.decoder] Now Idle->Executing
I/ACodec  (30704): [OMX.google.aac.decoder] Now Executing
I/ACodec  (30704): [OMX.google.aac.decoder] Now handling output port settings change
I/ACodec  (30704): [OMX.google.aac.decoder] Now Executing
2
W/.example.stori(30704): Accessing hidden method Landroid/media/AudioTrack$Builder;->setOffloadedPlayback(Z)Landroid/media/AudioTrack$Builder; (dark greylist, linking)

I posted the logs printing. Not sure any of the errors have to do with this issue however Expected behavior Values being returned in the currentmediaItem stream, not null on android when something is playing

Flutter SDK version

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.0.2, on macOS 11.2.3 20D91 darwin-x64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.1)
[✓] VS Code (version 1.54.3)
[✓] Connected device (1 available)

• No issues found!
ryanheise commented 3 years ago

HI @andy-t-wang Can you please

Which API doesn't behave as documented, and how does it misbehave? Name here the specific methods or fields that are not behaving as documented, and explain clearly what is happening.

Can you please fill in the above section?

Minimal reproduction project child: StreamBuilder<MediaItem?>( stream: AudioService.currentMediaItemStream, builder: (context, snapshot) { final mediaItem = AudioService.currentMediaItem; The audioplayertask code is the same as the example. I'm assuming the stream gets set at the onStart.

Can you please provide a link to your fork of audio_service that reproduces this issue?

andy-t-wang commented 3 years ago

@ryanheise Ok added the api. Not sure what you mean the fork of audio_serivce I'm just using this one from the flutter pages. Library Link. I didnt make any changes to the library. I know the queue is getting successfully update because things can play so the service is working for that, however, only the currentmediaitemstream and currentmediaitem are always null. This wasn't a problem when I used 0.16.2+1 version. After upgrading its a problem for only android, the currentmediaItemstream returns the mediaitem on my ios device

ryanheise commented 3 years ago

On GitHub, click the button to fork this project, then you will have your own repo with a copy of the whole code. Then, modify the example in the example subdirectory with minimal changes necessary to reproduce the bug. I will then be able to git clone your repo and run it. Then provide me with user steps on what I need to click on to cause the bug, and what I should expect to see in logs, etc.

ryanheise commented 3 years ago

Any update? I have gone ahead and released 0.17.0 now. If there is a bug, it will need to be fixed in a bug-fix release, but that will also depend on your reproduction project.

andy-t-wang commented 3 years ago

Yeah I'm still figuring it out I tried the new package but I'm struggling to make a repo to reproduce it. Which is weird since it works fine on the old version.

Codesait commented 3 years ago

Hello. I have a similar problem. Mine is that my ui state is not updating

ryanheise commented 3 years ago

@andy-t-wang as soon as you figure out how to reproduce it, that will be really helpful.

suragch commented 3 years ago

I've had what seems to be the same problem after upgrading to the null safe version. Both of these give null values:

AudioService.currentMediaItem
AudioService.currentMediaItemStream

I'm still trying to find the source of the problem. So far I've discovered that the example project with audio_service is working fine on the same Android device that my own project is not working on. This leads me to to think that it is something about how I initialize the playlist. I'll update again when I find the answer.

ryanheise commented 3 years ago

Interesting, yes this is something I've been curious about. I've seen it mentioned on StackOverflow but as yet I haven't seen it or been able to reproduce it myself.

suragch commented 3 years ago

I haven't been able to isolate the problem in my own code yet but it is interesting to note that the web version works fine while Android does not.

My next step is going to be to slowly add things into the example project until I find where it stops working.

Supplemental material:

This is my background audio service class:

import 'dart:async';

import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:just_audio/just_audio.dart';

void audioPlayerTaskEntrypoint() async {
  AudioServiceBackground.run(() => AudioPlayerTask());
}

class AudioPlayerTask extends BackgroundAudioTask {
  AudioPlayer _player = AudioPlayer();
  AudioProcessingState? _skipState;
  late StreamSubscription<PlaybackEvent> _eventSubscription;

  List<MediaItem> _queue = [];
  int? get index => _player.currentIndex;
  MediaItem? get mediaItem => index == null ? null : _queue[index!];

  @override
  Future<void> onStart(Map<String, dynamic>? params) async {
    _loadMediaItemsIntoQueue(params);
    await _setAudioSession();
    // _broadcaseMediaItemChanges();
    _propogateEventsFromAudioPlayerToAudioServiceClients();
    _performSpecialProcessingForStateTransistions();
    _loadQueue();
  }

  void _loadMediaItemsIntoQueue(Map<String, dynamic>? params) {
    _queue.clear();
    final List mediaItems = params?['data'];
    for (var item in mediaItems) {
      final mediaItem = MediaItem.fromJson(item);
      _queue.add(mediaItem);
    }
  }

  Future<void> _setAudioSession() async {
    final session = await AudioSession.instance;
    await session.configure(AudioSessionConfiguration.music());
  }

  // void _broadcaseMediaItemChanges() {
  //   _player.currentIndexStream.listen((index) {
  //     if (index != null) AudioServiceBackground.setMediaItem(_queue[index]);
  //   });
  // }

  void _propogateEventsFromAudioPlayerToAudioServiceClients() {
    _eventSubscription = _player.playbackEventStream.listen((event) {
      _broadcastState();
    });
  }

  void _performSpecialProcessingForStateTransistions() {
    _player.processingStateStream.listen((state) {
      switch (state) {
        case ProcessingState.completed:
          _player.pause();
          _player.seek(Duration.zero, index: _player.effectiveIndices?.first);
          break;
        case ProcessingState.ready:
          _skipState = null;
          break;
        default:
          break;
      }
    });
  }

  Future<void> _loadQueue() async {
    AudioServiceBackground.setQueue(_queue);
    try {
      await _player.setAudioSource(ConcatenatingAudioSource(
        useLazyPreparation: true,
        children: _queue.map((item) {
          final uri = Uri.parse(item.extras?['url']);
          return AudioSource.uri(uri);
        }).toList(),
      ));
      _player.durationStream.listen((duration) {
        _updateQueueWithCurrentDuration(duration);
      });
      //onPlay();
    } on PlayerException catch (e) {
      print("Error code: ${e.code}");
      print("Error message: ${e.message}");
      onStop();
    } on PlayerInterruptedException catch (e) {
      print("Connection aborted: ${e.message}");
      onStop();
    } catch (e) {
      print('Error: $e');
      onStop();
    }
  }

  void _updateQueueWithCurrentDuration(Duration? duration) {
    print('updating with new duration: $duration');
    final songIndex = _player.currentIndex;
    print('songIndex: $songIndex');
    if (songIndex == null || mediaItem == null || duration == null) {
      print('mediaItem: $mediaItem');
      return;
    }
    print(
        'current index: $songIndex, id: ${mediaItem!.id}, duration: $duration');
    final modifiedMediaItem = mediaItem!.copyWith(duration: duration);
    _queue[songIndex] = modifiedMediaItem;
    AudioServiceBackground.setQueue(_queue);
    AudioServiceBackground.setMediaItem(_queue[songIndex]);
  }

  @override
  Future<void> onSkipToQueueItem(String mediaId) async {
    final newIndex = _queue.indexWhere((item) => item.id == mediaId);
    if (newIndex == -1 || index == null) return;
    _skipState = newIndex > index!
        ? AudioProcessingState.skippingToNext
        : AudioProcessingState.skippingToPrevious;
    _player.seek(Duration.zero, index: newIndex);
  }

  @override
  Future<void> onPlay() => _player.play();

  @override
  Future<void> onPause() => _player.pause();

  @override
  Future<void> onSeekTo(Duration position) => _player.seek(position);

  @override
  Future<void> onFastForward() => _seekRelative(fastForwardInterval);

  @override
  Future<void> onRewind() => _seekRelative(-rewindInterval);

  @override
  Future<void> onSetRepeatMode(AudioServiceRepeatMode repeatMode) async {
    switch (repeatMode) {
      case AudioServiceRepeatMode.none:
        await _player.setLoopMode(LoopMode.off);
        break;
      case AudioServiceRepeatMode.one:
        await _player.setLoopMode(LoopMode.one);
        break;
      case AudioServiceRepeatMode.all:
      case AudioServiceRepeatMode.group:
        await _player.setLoopMode(LoopMode.all);
        break;
    }
  }

  @override
  Future<void> onStop() async {
    await _player.dispose();
    _eventSubscription.cancel();
    await _broadcastState();
    await super.onStop();
  }

  /// Jumps away from the current position by [offset].
  Future<void> _seekRelative(Duration offset) async {
    if (mediaItem?.duration == null) return;
    var newPosition = _player.position + offset;
    if (newPosition < Duration.zero) newPosition = Duration.zero;
    if (newPosition > mediaItem!.duration!) newPosition = mediaItem!.duration!;
    await _player.seek(newPosition);
  }

  /// Broadcasts the current state to all clients.
  Future<void> _broadcastState() async {
    await AudioServiceBackground.setState(
      controls: [
        MediaControl.skipToPrevious,
        if (_player.playing) MediaControl.pause else MediaControl.play,
        MediaControl.skipToNext,
      ],
      androidCompactActions: [0, 1, 2],
      processingState: _getProcessingState(),
      playing: _player.playing,
      position: _player.position,
      bufferedPosition: _player.bufferedPosition,
      speed: _player.speed,
    );
  }

  /// Maps just_audio's processing state into into audio_service's playing
  /// state. If we are in the middle of a skip, we use [_skipState] instead.
  AudioProcessingState _getProcessingState() {
    if (_skipState != null) return _skipState!;
    switch (_player.processingState) {
      case ProcessingState.idle:
        return AudioProcessingState.stopped;
      case ProcessingState.loading:
        return AudioProcessingState.connecting;
      case ProcessingState.buffering:
        return AudioProcessingState.buffering;
      case ProcessingState.ready:
        return AudioProcessingState.ready;
      case ProcessingState.completed:
        return AudioProcessingState.completed;
      default:
        throw Exception("Invalid state: ${_player.processingState}");
    }
  }
}

And this is my state management class:

import 'package:audio_service/audio_service.dart';
import 'package:myapp/models/song.dart';
import 'package:myapp/services/audio_service/background_audio_service.dart';
import 'package:rxdart/rxdart.dart';
import 'package:url_launcher/url_launcher.dart';

import 'audio_buttons/repeat_button_notifier.dart';
import 'favorites_row/favorite_button_notifier.dart';
import 'lyrics/lyrics_notifier.dart';
import 'seek_bar/seek_bar_notifier.dart';

class PlayPageState {

  final lyricsNotifier = LyricsNotifier();
  final repeatButtonNotifier = RepeatButtonNotifier();
  final favoriteButtonNotifier = FavoriteButtonNotifier();

  List<Song> get playlist => _playlist;
  List<Song> _playlist = [];

  Stream<PlaybackState> get playbackStateStream =>
      AudioService.playbackStateStream; //.map((state) => state.playing).distinct();

  Stream<bool> get isFirstMediaItemStream => AudioService.currentMediaItemStream
      .map((mediaItem) => mediaItem == AudioService.queue?.first)
      .distinct();

  Stream<bool> get isLastMediaItemStream => AudioService.currentMediaItemStream
      .map((mediaItem) => mediaItem == AudioService.queue?.last)
      .distinct();

  Stream<AudioPlayerState> get mediaStateStream =>
      Rx.combineLatest3<Duration, PlaybackState, MediaItem?, AudioPlayerState>(
          AudioService.positionStream,
          AudioService.playbackStateStream,
          AudioService.currentMediaItemStream,
          (position, playbackState, mediaItem) {
        lyricsNotifier.updateLyrics(mediaItem?.id, position);
        return AudioPlayerState(
          current: position,
          buffered: playbackState.bufferedPosition,
          total: mediaItem?.duration,
        );
      });

  Future<void> initAudioService(List<Song> playlist) async {
    _playlist = playlist;
    // load the lyrics
    lyricsNotifier.updateLyrics(playlist[0].id, Duration.zero);
    // load the favorite state
    favoriteButtonNotifier.lookupSavedFavoriteStatus(playlist[0].id);

    // set up the media service
    final mediaList = [];
    for (var song in playlist) {
      final mediaItem = MediaItem(
        id: song.id,
        title: song.name,
        album: '',
        extras: {'url': song.url},
      );
      mediaList.add(mediaItem.toJson());
    }
    if (mediaList.isEmpty) return;
    final params = {'data': mediaList};

    AudioService.start(
      backgroundTaskEntrypoint: audioPlayerTaskEntrypoint,
      androidStopForegroundOnPause: true,
      params: params,
    );

    _updateFavoriteStatus();
  }

  void _updateFavoriteStatus() {
    AudioService.currentMediaItemStream.listen((mediaItem) {
      if (mediaItem == null) return;
      favoriteButtonNotifier.lookupSavedFavoriteStatus(mediaItem.id);
    });
  }

  void onFavoriteButtonPressed() {
    print('currentMediaItem: ${AudioService.currentMediaItem}');
    final currentSongId = AudioService.currentMediaItem?.id;
    if (currentSongId == null) {
      return;
    }
    final song = playlist.firstWhere((song) => song.id == currentSongId);
    favoriteButtonNotifier.toggleState(song);
  }

  void onRepeatButtonPressed() {
    repeatButtonNotifier.changeState();
    final repeatState = repeatButtonNotifier.value;
    switch (repeatState) {
      case RepeatState.off:
        AudioService.setRepeatMode(AudioServiceRepeatMode.none);
        break;
      case RepeatState.repeatSong:
        AudioService.setRepeatMode(AudioServiceRepeatMode.one);
        break;
      case RepeatState.repeatPlaylist:
        AudioService.setRepeatMode(AudioServiceRepeatMode.all);
        break;
    }
  }

  void onPreviousSongButtonPressed() {
    if (!AudioService.running) {
      return;
    }
    AudioService.skipToPrevious();
  }

  Future<void> onPlayPauseButtonPressed() async {
    if (!AudioService.running) {
      return;
    }

    if (AudioService.playbackState.playing) {
      AudioService.pause();
    } else {
      AudioService.play();
    }
  }

  void onNextSongButtonPressed() {
    if (!AudioService.running) {
      return;
    }
    AudioService.skipToNext();
  }

  Future<void> disposeAudioService() async {
    AudioService.stop();
  }

  void onAudioPlayerSeek(Duration position) {
    AudioService.seekTo(position);
  }

  skipToSong(Song song) {
    if (!AudioService.running) {
      return;
    }
    // load the lyrics right away singe this may be faster than AudioService
    //lyricsNotifier.updateLyrics(song.id, Duration.zero);
    AudioService.skipToQueueItem(song.id);
  }

  void downloadCurrentSong() async {
    final url = AudioService.currentMediaItem?.extras?['url'];
    if (url == null) return;
    if (await canLaunch(url)){
      await launch(url);
    }
  }
}
letiagoalves commented 3 years ago

I can confirm there is an issue with currentMediaItemStream. I have just updated to 0.17.0 and now that broadcast stream is dead. I will debug it tomorrow to see what caused this.

letiagoalves commented 3 years ago

I've found the issue. The culprit is this function:

private static String metadataToString(MediaMetadataCompat mediaMetadata, String key) {
        CharSequence value = mediaMetadata.getText(key);
        if (value != null && value.length() > 0)
            return value.toString();
        return null;
    }

when we define album as an empty String, it gets back as null from the Android native code because value.length() == 0. However, because album is marked as not nullable now, it throws an exception in the dart side in the following function:

factory MediaItem.fromJson(Map raw) => MediaItem(
        id: raw['id'],
        album: raw['album'],
        title: raw['title'],
        artist: raw['artist'],
        genre: raw['genre'],
        duration: raw['duration'] != null
            ? Duration(milliseconds: raw['duration'])
            : null,
        artUri: raw['artUri'] != null ? Uri.parse(raw['artUri']) : null,
        playable: raw['playable'],
        displayTitle: raw['displayTitle'],
        displaySubtitle: raw['displaySubtitle'],
        displayDescription: raw['displayDescription'],
        rating: raw['rating'] != null ? Rating._fromRaw(raw['rating']) : null,
        extras: raw['extras']?.cast<String, dynamic>(),
      );

I will create a PR tomorrow to fix this.

letiagoalves commented 3 years ago

@ryanheise before proceeding with the PR, can you give me more context on why metadataToString function is casting empty strings to null?

ryanheise commented 3 years ago

Ah, interesting. I'll look into it now, but this also suggests a workaround for everybody which is to set the album string to a non-empty whitespace string (e.g. " " - with one space).

ryanheise commented 3 years ago

OK, so I think from memory that I noticed a bug in Android where it was unable to distinguish between an empty string and a null. But looking at the code, I honestly can't remember the finer details of that now. Maybe Android was returning empty strings for some metadata even though the app may have set them as nulls, or maybe it was something else.

Note that metadataToString is used not only for title but also nullable metadata like displayTitle.

Overall, the ideal solution is one that preserves the null or empty string value from the time it was inputted to the time it is received through broadcasts. In the one-isolate branch this is less of an issue because we now bypass the native layer and the UI listens directly to the same stream that the audio logic emits, in the same isolate. But in the master branch it does go through the native layer. If you print out the values of these null and empty string metadata values before and after they go through this native layer, you might be able to confirm what is happening here.

For example:

            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) {
                invokeMethod("onMediaChanged", mediaMetadata2raw(metadata));
            }

If you log at this point, you will be able to see if Android has altered the null or empty string metadata before we deliver that event to the Flutter app.

ryanheise commented 3 years ago

Found the original issue for context: #363

letiagoalves commented 3 years ago

Thanks @ryanheise. I created a PR to address this. I am already using a version with that fix and testing it on my real devices. I will let you know if I find any issue.

hpapier commented 3 years ago

Hello I'm also facing an issue about the currentMediaStream on android. Everything works well without any navigation, but after using the back arrow of the android device it seems that the setMediaItem function doesn't send an event anymore. I handle the android's back arrow button with an OnWillPop that pop my pages and return false. Idk why this occurs, it worked perfectly before the null-safety version. Does anyone faces this issue too ?

suragch commented 3 years ago

I'm in the process of migrating to the one-isolate branch. If that solves the issue then trying to fix the bug in version 0.17 is probably a mute point since I understand this will be the future version 0.18.

ryanheise commented 3 years ago

@hpapier if you read above you'll see both a workaround and a fix. Please try one of those as preferred and see how it goes.

hpapier commented 3 years ago

@ryanheise Do you think that this issue comes from the null-safety values instead of another bug ? Because without any navigation, so without any use of the back arrow button on android, everything works fine, the mediaItems are correctly sent, but when the button back arrow is pressed and handled by a WillPopScope that return a false value, then it seems that all the streams connected to the UI that comes from the AudioService are disconnected or lost. So the StreamBuilders don't receive the events anymore, but the mediaItem is correctly set in the notification information etc... any ideas ? :)

ryanheise commented 3 years ago

@hpapier Try either the workaround or the fix above and see if it fixes your issue first.

hpapier commented 3 years ago

@ryanheise Hey! As expected I tried with the fix #664 and it didn't work (because I always set the album field to a non empty string). The problem here is really the back arrow button that breaks the streams between the audio service isolate (I guess) and the UI, even the playbackStateStream doesn't work anymore after a use of the android's back button.

ps: I use BLoC archi which uses a lot of streams and no ones break because of the android's back button. Any idea of what could it be ?

ryanheise commented 3 years ago

@hpapier maybe you're disconnecting from audio service in your app when pressing the back button. It sounds like maybe an app logic/navigation issue rather than this bug (which is related to null values).

hpapier commented 3 years ago

@ryanheise Not really an app logic/navigation issue, it worked before this null-safety version, everything work on IOS even with the navigation. (Not related to the null values but related to the null-safety version) :)

ryanheise commented 3 years ago

@hpapier it still appears to be a distinct bug though, as it is not solved by this fix. Would you mind opening a new issue with your own reproduction project?

nt4f04uNd commented 3 years ago

Here's the commit that introduced the string size check

https://github.com/ryanheise/audio_service/commit/3a4927aa312ca9dcc6599195c051a971e8c124b4#diff-02483e72573030464a0602590d37c1f717c0b315d99638a3fc6d015541d5d5a9

It looks to me like exception it was supposed to fix was fixed by the null check, whilst the size check is unnecessary. Empty string metadata should be completely legal

image

nt4f04uNd commented 3 years ago

Could someone who was having an issue confirm it's resolved on the one-isolate head?

suragch commented 3 years ago

The one-isolate branch is working for me on Android. I'm one of the ones who was having trouble earlier.

ryanheise commented 3 years ago

I will close now that #721 is merged. If anyone still has this issue on the one-isolate branch, leave a message below with details and I will reopen.

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs, or use StackOverflow if you need help with audio_service.