Canardoux / flutter_sound

Flutter plugin for sound. Audio recorder and player.
Mozilla Public License 2.0
850 stars 553 forks source link

[BUG]: openPlayer() causing exception #1041

Open mountaindu opened 3 weeks ago

mountaindu commented 3 weeks ago

Flutter Sound Version :

Severity


Platforms you faced the error


Describe the bug Simply calling openPlayer() causes an exception: "Null check operator used on a null value"

Call stack:

MethodChannelFlutterSoundPlayer.setCallback.<anonymous closure> (/Users/alexdu/.pub-cache/hosted/pub.dev/flutter_sound_platform_interface-9.4.13/lib/method_channel_flutter_sound_player.dart:45)
MethodChannel._handleAsMethodCall (/Users/alexdu/flutter/packages/flutter/lib/src/services/platform_channel.dart:571)
MethodChannel.setMethodCallHandler.<anonymous closure> (/Users/alexdu/flutter/packages/flutter/lib/src/services/platform_channel.dart:564)
_DefaultBinaryMessenger.setMessageHandler.<anonymous closure> (/Users/alexdu/flutter/packages/flutter/lib/src/services/binding.dart:581)
_invoke2 (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/hooks.dart:344)
_ChannelCallbackRecord.invoke (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/channel_buffers.dart:45)
_Channel.push (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/channel_buffers.dart:135)
ChannelBuffers.push (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/channel_buffers.dart:343)
PlatformDispatcher._dispatchPlatformMessage (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/platform_dispatcher.dart:750)
_dispatchPlatformMessage (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/ui/hooks.dart:257)

My code is quite straightforward:

MyWidget extends HookWidget {
  final FlutterSoundPlayer _soundPlayer = FlutterSoundPlayer();
  ...
  useEffect(() {
       // This call consistently raises an exception. Doesn't matter whether completely async or not.
        _soundPlayer.openPlayer().then((value) {
          // do something
        });
      }, []);
}

To Reproduce Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Logs!!!!

flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   new FlutterSoundPlayer (package:flutter_sound/public/flutter_sound_player.dart:130:13)
flutter: │ #1   new MyWidget (package:app/ui/chat_v2/chat.dart:381:48)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 ctor: FlutterSoundPlayer()
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer._openAudioSession (package:flutter_sound/public/flutter_sound_player.dart:507:13)
flutter: │ #1   FlutterSoundPlayer.openPlayer.<anonymous closure> (package:flutter_sound/public/flutter_sound_player.dart:501:11)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 FS:---> openAudioSession
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer._openAudioSession (package:flutter_sound/public/flutter_sound_player.dart:520:15)
flutter: │ #1   FlutterSoundPlayer.openPlayer.<anonymous closure> (package:flutter_sound/public/flutter_sound_player.dart:501:11)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 Resetting flutter_sound Player Plugin
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.log (package:flutter_sound/public/flutter_sound_player.dart:358:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:127:19)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 IOS:--> initializeFlautoPlayer
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Larpoux commented 3 weeks ago

@mountaindu ,

Flutter Sound 9.4.14 is released. It fixes something inside method_channel_flutter_sound_player.dart and method_channel_flutter_sound_recorder.dart.

Please let me known if this fixes your problem or if not, send the new logs.

mountaindu commented 3 weeks ago

Yes that works, thanks for the patch!

I’m running into a separate similar issue now in the MethodChannelPlayer when closing the player. This happens when calling FlutterSoundPlayer.closePlayer() both when the player is actively playing audio or inactive.

Here's the stack trace:

FlutterSoundPlayerPlatform.getSession (/Users/alexdu/.pub-cache/hosted/pub.dev/flutter_sound_platform_interface-9.4.14/lib/flutter_sound_player_platform_interface.dart:110)
MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (/Users/alexdu/.pub-cache/hosted/pub.dev/flutter_sound_platform_interface-9.4.14/lib/method_channel_flutter_sound_player.dart:53)
new Future.<anonymous closure> (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/async/future.dart:257)
Timer._createTimer.<anonymous closure> (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/timer_patch.dart:18)
_Timer._runTimers (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/timer_impl.dart:398)
_Timer._handleMessage (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/timer_impl.dart:429)
_RawReceivePort._handleMessage (/Users/alexdu/flutter/bin/cache/pkg/sky_engine/lib/_internal/vm/lib/isolate_patch.dart:184)

And logs prior to.


flutter: │ #0   FlutterSoundPlayer.log (package:flutter_sound/public/flutter_sound_player.dart:358:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:129:21)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 iOS: invokeMethod stopPlayerCompleted - state=0
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.stopPlayerCompleted (package:flutter_sound/public/flutter_sound_player.dart:294:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:103:21)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 ---> stopPlayerCompleted: true
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.stopPlayerCompleted (package:flutter_sound/public/flutter_sound_player.dart:309:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:103:21)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 <--- stopPlayerCompleted: true
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.log (package:flutter_sound/public/flutter_sound_player.dart:358:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:129:21)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 IOS:<-- stopPlayer
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.log (package:flutter_sound/public/flutter_sound_player.dart:358:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:129:21)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 IOS:<-- stopPlayer
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer.log (package:flutter_sound/public/flutter_sound_player.dart:358:13)
flutter: │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:129:21)
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 iOS: invokeMethod needSomeFood - state=0
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
flutter: │ #0   FlutterSoundPlayer._closeAudioSession (package:flutter_sound/public/flutter_sound_player.dart:600:13)
flutter: │ #1   <asynchronous suspension>
flutter: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
flutter: │ 🐛 FS:<--- closeAudioSession
flutter: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Larpoux commented 3 weeks ago

I am going to look at that tomorrow. Thank you for having sent the logs : this is really helpful for debugging.

Larpoux commented 3 weeks ago

No, this is not the same bug already fixed. I already fixed the bug for all API verbs, both for Player and Recorder in 9.4.14

Are you sure that you call stopPlayer() in the main Isolate ?

mountaindu commented 3 weeks ago

I'm not sure what you mean by "main Isolate" but i don't think the call to stopPlayer() is relevant as it's the closePlayer that's causing issues. I can repro by doing a simple call sequence of:

await _soundPlayer.openPlayer();
await _soundPlayer.startPlayerFromStream(...)
await _soundPlayer.closePlayer();
Larpoux commented 3 weeks ago

I will look tomorrow. The logs show that stopPlayer is called correctly by close player. Then stop player returns and then closePlayer returns. This seems ok. But I don’t like that iOS asks for some audio data just after it is closed. This is not correct.

Also I think that you have a timer that calls flutter sound. I want to be sure that this timer is fired in the main thread.

Larpoux commented 3 weeks ago

This is not really a crash. Flutter Sound throws an exception but you don't catch it. The problem is that someone is trying to access the player after it has been closed. It seems that it is from a timer.

mountaindu commented 3 weeks ago

Thanks for the investigation, I'm using a StreamSubscription which I believe uses Timers under the hood so you're right that there could be a bad access there. Is there some check on the FlutterSoundPlayer object that I can use to tell whether it's been closed? Or should I manage that state myself.

While I'm here, two other things I'm curious about -- 1) The backpressure variant .feedFromStream() causes a native iOS crash. I can provide the stack trace here later. 2) When using the foodSink version, are the sinks opearting as a queue? When I add a stopPlayer event to the sink, my last audio chunk doesn't play.

Here is my rough code:

class AudioStreamPlayer {
  final FlutterSoundPlayer _soundPlayer = FlutterSoundPlayer();
  StreamSubscription<Uint8List?>? _audioSubscription;
  bool _isInit = false;

  Future<bool> init() async {
    try {
      await _soundPlayer.openPlayer();
      _isInit = true;
      return true;
    } catch (e) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: e,
        stack: StackTrace.current,
        library: "AudioStreamPlayer.init",
      ));
      return false;
    }
  }

  Future<void> start(ServerTypedJsonStreamWrapper streamWrapper,
      Stream<Uint8List?> audioStream) async {
    if (!_isInit) {
      await init();
    }
    if (_soundPlayer.isStopped) {
      await _soundPlayer.startPlayerFromStream(
        codec: Codec.pcm16,
        numChannels: 1,
        sampleRate: 16000,
        bufferSize: 20480,
      );
    }

    _audioSubscription = audioStream.listen((audioBytes) {
      if (audioBytes != null) {
        int lnData = 0;
        int totalLength = audioBytes.length;

        while (totalLength > 0 && !_soundPlayer.isStopped) {
          var bsize = totalLength > blockSize ? blockSize : totalLength;
          // Not using backpressure. Should be using async .feedFromStream()
          // instead of this foodSink thing but it crashed when I try.
          _soundPlayer.foodSink!
              .add(FoodData(audioBytes.sublist(lnData, lnData + bsize)));

          lnData += bsize;
          totalLength -= bsize;
        }
      }
    }, onDone: () {
      // Hm this cuts off the last audiobyte chunk from playing:
      // _soundPlayer.foodSink!.add(FoodEvent(() {
      //   _soundPlayer.stopPlayer();
      // }));
    });
  }

  void close() {
    _soundPlayer.closePlayer();
  }

  Future<void> dispose() async {
    await _soundPlayer.closePlayer();
    if (_audioSubscription != null) {
      await _audioSubscription!.cancel();
    }
  }
}
Larpoux commented 3 weeks ago

I looked to your code very rapidly. My first answer without having looked seriously to your code:

  1. It seams that you are receiving the audio data from an external stream. Working with back pressure means that you await flutter sound asking the data. Because you receive the data asynchronously from another stream, you will have to manage a buffer between the two streams. Not very simple and not very good: you will just be coding what an output stream does for you. Stupid. Better to send the data to the sink when you receive them, as you actually do.
  2. This is bad that awaiting flutter sound asking the data crash the app. You are not concerned but someone should debug this crash. Unfortunately me! ;-)
  3. When you add your data to the sink, you don’t wait. It is the stream job to buffer the data if you add them faster than flutter sound is able to play them. In fact this is exactly what you want.
  4. When you want to stop the player, it would be good if you can specify if you want to flush the buffered data, or if you want to delay this stop until the sink is empty. This is not actually possible with the current API.
  5. When the player is closed, this is flutter sound responsibility to flush and delete the sink, so that we don’t try to play the buffered data to a closed player

I am actually rewriting all the code for streams. First on iOS. This is a major work, and I am very slow. And difficult for me to code this v10.0 because I spend too much time on flutter sound maintenance.

Larpoux commented 3 weeks ago

@mountaindu : Please, could you try new version 9.4.19 of Flutter Sound ? I protect the internal feed() function against a stopPlayer() during the loop.

Larpoux commented 3 weeks ago

@mountaindu : Please, could you try new version 9.4.19 of Flutter Sound ? I protect the internal feed() function against a stopPlayer() during the loop.

It seems that this patch fixed also the Play From Stream With Back-pressure(). You are not concerned but I wanted to let you know that this patch seems OK.

mountaindu commented 3 weeks ago

Has the patch updated the underlying min iOS version for flutter_sound_core? I'm getting the following error when trying to upgrade to the new version:

[!] CocoaPods could not find compatible versions for pod "flutter_sound_core":
      In snapshot (Podfile.lock):
        flutter_sound_core (= 9.4.14)

      In Podfile:
        flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) was resolved to 9.4.19, which
        depends on
          flutter_sound_core (= 9.4.19)

    Specs satisfying the `flutter_sound_core (= 9.4.14), flutter_sound_core (= 9.4.19)` dependency were
    found, but they required a higher minimum deployment target.

    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:317:in
    `raise_error_unless_state'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:299:in `block in
    unwind_for_conflict'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:297:in `tap'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:297:in
    `unwind_for_conflict'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:257:in
    `process_topmost_state'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolution.rb:182:in `resolve'
    /Library/Ruby/Gems/2.6.0/gems/molinillo-0.8.0/lib/molinillo/resolver.rb:43:in `resolve'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/resolver.rb:94:in `resolve'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1082:in `block
    in resolve_dependencies'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:1080:in
    `resolve_dependencies'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer/analyzer.rb:125:in `analyze'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:422:in `analyze'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:244:in `block in
    resolve_dependencies'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/user_interface.rb:64:in `section'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:243:in
    `resolve_dependencies'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/installer.rb:162:in `install!'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/command/install.rb:52:in `run'
    /Library/Ruby/Gems/2.6.0/gems/claide-1.1.0/lib/claide/command.rb:334:in `run'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/lib/cocoapods/command.rb:52:in `run'
    /Library/Ruby/Gems/2.6.0/gems/cocoapods-1.15.2/bin/pod:55:in `<top (required)>'
    /usr/local/bin/pod:23:in `load'
    /usr/local/bin/pod:23:in `<main>'

Error output from CocoaPods:
↳
    /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin23/rbconf
    ig.rb:21: warning: Insecure world writable dir /usr/local/bin in PATH, mode 040777
    /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin23/rbconf
    ig.rb:21: warning: Insecure world writable dir /usr/local/bin in PATH, mode 040777

Error: The pod "flutter_sound_core" required by the plugin "flutter_sound" requires a higher minimum
iOS deployment version than the plugin's reported minimum version.
To build, remove the plugin "flutter_sound", or contact the plugin's developers for assistance.
Error running pod install
Larpoux commented 3 weeks ago

No, I did not do that intentionally. I am going to fix that immediately. Just wait an hour or so.

Larpoux commented 3 weeks ago

[!] CocoaPods could not find compatible versions for pod "flutter_sound_core": In snapshot (Podfile.lock): flutter_sound_core (= 9.4.14)

This is not good. Do you use your own version of flutter_sound_core ? it should be 9.4.19. Flutter_sound_core version and Flutter_sound version are synchronised to simplify the maintenance

mountaindu commented 2 weeks ago

Ah needed to delete some stale iOS artifacts, all is fixed now thank you!

Feel free to close this issue out but one more small request while I’m here: it'd be really great if there was some "isActivelyPlaying" listener for the streaming use case (i.e. when there's no more Food events playing). This seems like it’d be relatively simple change and would be a great feature addition. In my particular use case, we want to render a UI element only when audio is playing.

Larpoux commented 2 weeks ago

Not sure that it will answer to your need, but there is something which can be useful when working with PlayFromStreamWithoutBackpressure : You add audio data to the stream without waiting that the player is ready. So the data are buffered by the stream. But you don’t know when the data are really played. With Flutter sound, you can insert in your sink not only audio data, but also "events". Those events are put into the stream, and will be consumed by flutter sound when all the previous audio data are also consumed (played). Then Flutter sound call a playback to the app, to tell it that it is now synchronized with your checkpoint.

The example uses this mechanism to know that all the previous data are consumed (played) and that it is ok to close the player.

 _mPlayer.foodSink!.add(FoodEvent(() async {
      await _mPlayer.stopPlayer();
      setState(() {});
    }));

My explanation is hard, but the mechanism is really simple. The stream is composed with several data, and checkpoints in the middle. When the checkpoint is consumed your callback is fired.

If you have questions, don’t hesitate to tell me.

Larpoux commented 2 weeks ago

Note that the events (the markers) that you insert into your stream are not played. This are just indicators to fire a callback when consumed

Not also that when you receive your callback, it does not mean that the stream is empty because you could have posted new data after the marker. If you need to know that, I can implement it. Simple.

Larpoux commented 2 weeks ago

I think a little bit more on your needs. I could implement a back stream. I will post an event each time the device is ready to play something. You will receive on this stream many events during the playback. If the playback stop (because all the data has been played and the app has not sent new data), then the stream will be quiet until the app send new data.

Is it something useful ?

mountaindu commented 2 weeks ago

If the foodevent executes after the last audio has been played, that'd fit my needs but I think there's a bug/edge case. The problem I saw (you can see it commented out in my code here) is that adding the stopPlayer() call in the food event ends up cutting the last audio chunk.

Without diving into the code, my guess is that it's because the last audio element gets popped from the Food stream to start playing and then the stopPlayer() event gets popped as well, but executes async before the last audio element finishes playing.

I could implement a back stream. I will post an event each time the device is ready to play something. You will receive on this stream many events during the playback. If the playback stop (because all the data has been played and the app has not sent new data), then the stream will be quiet until the app send new data.

This wouldn't be quite ideal. The problem is the as a listener to the stream, I don't know how frequently events are coming back or if there's any guarantees there: the stream could be quiet for a while but maybe there's been something wrong downstream.

I think I basically want a callback once the underlying buffer finishes playing in this callback:

https://github.com/Canardoux/flutter_sound_core/blob/2f88b59d7eaf4ac16a48ff6ea4b107b2791d24d3/ios/Classes/FlautoPlayerEngine.mm#L337?352

Larpoux commented 2 weeks ago

Yes, I think that the actual API is enough for you. Yes, there is a problem with the last buffer: I call the callback before it is really played. This is bad and I am going to try to find a solution for that.

Larpoux commented 2 weeks ago

@mountaindu : I added a new parameter for Start Player From Stream in Flutter Sound 9.6.0: whenFinished:. This call back is called when our own buffers are empty. Unfortunately I cannot do anything for the buffers managed by the device itself. Eventually you can diminish the buffer size, but be careful : if too small you may loose some audio packets, and it will increase the CPU load.

I did some tests and it seems to work correctly but the code is tricky and you will have to test that it works well. Please let me know if it is OK for you.

mountaindu commented 1 week ago

Just did a little testing and it works great, exactly what I need -- thanks again!

mountaindu commented 1 week ago

@Larpoux ah actually the whenFinished callback doesn't seem to be working on Android, seeing this both on sim and on device. iOS works well.

Another Android-specific issue I'm seeing is that replaying after closing (specifically init or startPlayerFromStream) is a no-op with no debug logs printed. Does having a singleton sound player object not work on Android?

final FlutterSoundPlayer _soundPlayer = FlutterSoundPlayer()
await _soundPlayer.openPlayer();
await _soundPlayer.startPlayerFromStream(...)
_soundPlayer.foodSink!.add(...) // This plays audio normally
await _soundPlayer.closePlayer();

// Both of these actions do nothing on Android
await _soundPlayer.openPlayer();
await _soundPlayer.startPlayerFromStream(...)

Some logs from the closeplayer call.

I/flutter ( 5307): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter ( 5307): │ #0   FlutterSoundPlayer.stopPlayerCompleted (package:flutter_sound/public/flutter_sound_player.dart:313:13)
I/flutter ( 5307): │ #1   MethodChannelFlutterSoundPlayer.channelMethodCallHandler.<anonymous closure> (package:flutter_sound_platform_interface/method_channel_flutter_sound_player.dart:103:21)
I/flutter ( 5307): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter ( 5307): │ 🐛 ---> stopPlayerCompleted: true
I/flutter ( 5307): └────────────────────────────────────────────────────────I/flutter ( 5307): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter ( 5307): │ #0   FlutterSoundPlayer._closeAudioSession (package:flutter_sound/public/flutter_sound_player.dart:619:13)
I/flutter ( 5307): │ #1   <asynchronous suspension>
I/flutter ( 5307): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter ( 5307): │ 🐛 FS:<--- closeAudioSession 
I/flutter ( 5307): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Larpoux commented 1 week ago

I am going to look to that. I am not surprised that the things are not going for Android. Thank you for your tests.

Larpoux commented 1 week ago

OK : there is a bug on closePlayer(). At least on Android. I am going to fix it. But, I think that your code can be improved : Do not close the player if you don't have finished everything with it. Just do some startPlayer() and stopPlayer() When you have finished with your player (probably in dispose() ) you can close it.

  @override
  void dispose() {
    _mPlayer.stopPlayer();
    _mPlayer.closePlayer();
     super.dispose();
  }