ryanheise / just_audio

Audio Player
1.02k stars 625 forks source link

[Chrome] Continuously stream realtime PCM with `StreamAudioSource` #1028

Open yaakovschectman opened 1 year ago

yaakovschectman commented 1 year ago

Which API doesn't behave as documented, and how does it misbehave? When playing a StreamAudioSource that returns responses supporting range requests, with null offset and lengths, and returning WAV PCM data with a header indicating the total length is greater than the returned data, no subsequent range requests are made to the source. Tested in Chrome.

Minimal reproduction project https://github.com/yaakovschectman/flutter-just-audio-test

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

  1. run flutter run -d chrome in the above project.

Error messages No error message.

Expected behavior I expect that the player would send an initial request to the StreamAudioSource and receive a response that indicates the source a) supports range requests, and b) still has more data to send. This should result in an effectively continuous sound playing when the button is pressed, as the player keeps requesting new data to play. Instead, the sound plays for 1 second (the duration of the initial request's response), and then goes silent. A print debug reveals no subsequent requests are received until the button is pressed again, calling play() again.

Screenshots No applicable screenshots.

Desktop (please complete the following information):

Smartphone (please complete the following information): Not tested on smartphone.

Flutter SDK version

Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel [user-branch], 3.12.0-8.0.pre.137, on Microsoft Windows [Version 10.0.19045.3208], locale en-US)
    ! Flutter version 3.12.0-8.0.pre.137 on channel [user-branch] at C:\src\flutter\flutter\flutter
      Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
      If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/docs/get-started/install.
    ! Upstream repository git@github.com:yaakovschectman/flutter.git is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to git@github.com:yaakovschectman/flutter.git to dismiss this error.
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0) 
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Professional 2022 17.5.1)
[√] Android Studio (version 2021.3)
[√] VS Code (version 1.78.2)
[√] Connected device (3 available)
[√] Network resources

Additional context As an example application, suppose I am developing an application that generates audio in realtime and plays it over the speakers, such a DSP application or synthesizer. In this case, I would need to continuously stream small buffers of fresh audio data from the source to the player. From the documentation, it seems this should be done by subclassing StreamAudioSource to return the chunks of data to the requester. I've not been able to figure out how to make this actually work, or if there is a preferred method instead.

db-dblank commented 8 months ago

I have a similar scenario where I need to stream audio coming from a websocket. We are using ElevenLabs and the audio comes in small chunks from a websocket.

class StreamSource extends StreamAudioSource {
  StreamSource(this.stream);

  final Stream<List<int>> stream;

  @override
  Future<StreamAudioResponse> request([int? start, int? end]) async {
    return StreamAudioResponse(
      sourceLength: null,
      contentLength: null,
      offset: 0,
      stream: stream,
      contentType: 'audio/mp3',
    );
  }
}

final audioPlayer = AudioPlayer();
final streamCtrl = StreamController<List<int>>.broadcast();
final ws = WebSocketChannel.connect(Uri.parse('wss://example.com'));

ws.stream.listen((event) async {
  if (event.type == 'start') {
    await audioPlayer.setAudioSource(StreamSource(streamCtrl.stream));
    await audioPlayer.play();
  }
  streamCtrl.sink.add(base64Decode(event.audio));
});

No audio is being played this way. If a play each chunk separately by setting setAudioSource for each with another StreamAudioSource implementation, it plays but there is a gap between each audio chunk.

@ryanheise is there a way this could be done? Am I doing something wrong? Thanks

ryanheise commented 8 months ago

StreamAudioSource was originally designed to be used with the proxy server which only runs on Android, iOS and desktop. It is not possible to run a proxy server on web. So the interpretation of StreamAudioSource on web is a bit more limited. All it can do is make the whole request in one go as a normal request, and it then turns that into a data URI. It's good enough for loading small assets, but not much else.

I don't have any good ideas on how to do something better for web, but would welcome a contribution if anyone else wants to improve it.

artyomkonyaev commented 7 months ago

Hello @yaakovschectman ,

I am currently facing a problem that is very similar to what you described here.. Have you been able to solve this problem?

I would grateful if you could share more details about your current approach, possibly with code snippets. Even if you are utilizing something else than just_audio (perhaps such knowledge could later help improve this package's capabilities).

Thank you.

ryanheise commented 7 months ago

For someone who wants to have a go at implementing this feature, the way to do it would probably be to use the web audio API.

The current implementation is an HTML5 player:

/// An HTML5-specific implementation of [JustAudioPlayer].
class Html5AudioPlayer extends JustAudioPlayer {

Someone would need to create a Web Audio subclass and an option to select which web implementation to use for a new instance. Note that Web Audio and HTML5 have their own strengths and weaknesses. For example, Web Audio can only play audio with the correct CORS headers so it is not a good option for anyone playing audio from servers not under their own control. Web Audio also can't adjust the speed of audio playback. However, Web Audio can read and write the audio data directly, and so it would permit a feature such as this. I did create an early implementation of Web Audio in the first draft of just_audio_web but ditched it since on balance HTML5 offered closer feature parity with the other platforms for the features the plugin offers. I don't know if I still have the old implementation but if I were recreating it today, I'd probably use web. It's not a high priority for me, however, so this would make an open task for a potential contributor.

There is a PR migrating the current HTML5 code to use web (#1136 ), so that would be a starting point for adding another subclass for web audio.

fvisticot commented 4 weeks ago

Any news on this Web implementation ?