ryanheise / just_audio

Audio Player
1.04k stars 667 forks source link

Proxy Handler triggered two times in StreamAudioSource #1195

Open JakeSeo opened 8 months ago

JakeSeo commented 8 months ago

Which API doesn't behave as documented, and how does it misbehave? I tried to create my own StreamAudioSource, and injected it as the audio source for my streaming audio.

It works but it seems that last few chunks of the stream is not being played. I tried to check the library code and the part where local audio server is created and listens to incoming requests, the corresponding proxy handler for the audio streaming is triggered two times, when the stream starts and ends.

I don't understand why the handler is triggered when the stream ends. When the handler is triggered, the stream is already finished, so it closes the listeners right away. At this point, audio is not finished so the player cuts the audio before the last few chunks are played. Is this suppose to happen? or am I missing something?

here's the code that I think causes the problem:

// from just_audio.dart
...
  /// Starts the server.
  Future<dynamic> start() async {
    _running = true;
    _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
    _server.listen((request) async {
      if (request.method == 'GET') {                        // this listener gets triggered when the stream ends.
        final uriPath = _requestKey(request.uri);
        final handler = _handlerMap[uriPath]!;
        handler(this, request); 
      }
    }, onDone: () {
      _running = false;
    }, onError: (Object e, StackTrace st) {
      _running = false;
    });
  }

...

/// The type of functions that can handle HTTP requests sent to the proxy.
typedef _ProxyHandler = void Function(
    _ProxyHttpServer server, HttpRequest request);

/// A proxy handler for serving audio from a [StreamAudioSource].
_ProxyHandler _proxyHandlerForSource(StreamAudioSource source) {
  Future<void> handler(_ProxyHttpServer server, HttpRequest request) async {
    final rangeRequest =
        _HttpRangeRequest.parse(request.headers[HttpHeaders.rangeHeader]);

    request.response.headers.clear();

    StreamAudioResponse sourceResponse;
    Stream<List<int>> stream;
    try {
      sourceResponse =
          await source.request(rangeRequest?.start, rangeRequest?.endEx);
      stream = sourceResponse.stream;
    } catch (e, st) {
      // ignore: avoid_print
      print("Proxy request failed: $e\n$st");

      request.response.headers.clear();
      request.response.statusCode = HttpStatus.internalServerError;
      await request.response.close();
      return;
    }

    request.response.headers
        .set(HttpHeaders.contentTypeHeader, sourceResponse.contentType);

    if (sourceResponse.rangeRequestsSupported) {
      request.response.headers.set(HttpHeaders.acceptRangesHeader, 'bytes');
    }

    if (rangeRequest != null && sourceResponse.offset != null) {
      final range = _HttpRangeResponse(
          sourceResponse.offset!,
          sourceResponse.offset! + sourceResponse.contentLength! - 1,
          sourceResponse.sourceLength);
      request.response.contentLength = range.length ?? -1;
      request.response.headers
          .set(HttpHeaders.contentRangeHeader, range.header);
      request.response.statusCode = 206;
    } else {
      request.response.contentLength = sourceResponse.contentLength ?? -1;
      request.response.statusCode = 200;
    }

    final completer = Completer<void>();
    final subscription = stream.listen((event) {
      request.response.add(event);
    }, onError: (Object e, StackTrace st) {
      source._player?._playbackEventSubject.addError(e, st);
    }, onDone: () {
      completer.complete();
    });

    request.response.done.then((dynamic value) {
      subscription.cancel();
    });

    await completer.future;

    await request.response.close();        // it closes the response while audio is still playing since
  }                                                              //  it is called when the stream ends??? not sure..

  return handler;
}
...

you can see my full code below:

Minimal reproduction project https://github.com/JakeSeo/just_audio

To Reproduce (i.e. user steps, not code) Steps to reproduce the behavior: run the example code of just_audio inside the repository above.

Error messages

Expected behavior The audio is played until the end without being cut down.

Screenshots If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

Smartphone (please complete the following information):

Flutter SDK version

 % flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.16.9, on macOS 13.6.2 22G320 darwin-arm64, locale en-KR)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Android Studio (version 2021.2)
[✓] VS Code (version 1.86.2)
[✓] Connected device (1 available)
[✓] Network resources

Additional context

ryanheise commented 8 months ago

See the instructions below:

Minimal reproduction project Provide a link here using one of two options:

  1. Fork this repository and modify the example to reproduce the bug, then provide a link here.
  2. If the unmodified official example already reproduces the bug, just write "The example".
JakeSeo commented 8 months ago

I updated the issue. What I'm actually trying to do is request tts from Azure Open AI and the last part of the result is always cut. I gave you my full code, I will be disposing the api-key once this issue will be resolved. Help me please. thank you..

ryanheise commented 8 months ago

Before looking into your hypothesis, I am just looking at your implementation of StreamSource:

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: null,
      stream: stream,
      contentType: 'audio/mpeg',
      rangeRequestsSupported: false,
    );
  }
}

An implementation should normally handle the case where multiple requests are made.

I see in your bug report that you are mystified about why multiple requests would be made, but those requests are outside my control, they originate from the native player module (in this case, ExoPlayer). I also can't say I know why ExoPlayer is making multiple requests (particularly since you disabled range requests), but debug printing out the request headers might answer those questions.

I am considering between whether that's just part of the mysterious behaviour of ExoPlayer which you need to adapt to, or whether somewhere in my code I am ignoring your request to disable range requests.

JakeSeo commented 8 months ago

I just feel that multiple requests is the problem but I'm not sure if it actually is.

The actual problem is that the audio is not being played until the end. Few of the last chunks of the stream are not being played. If I may refer to my example code, I request Text to Speech request with the text that ends with "..... fine-tune custom models.", the audio is only played until "..... fine-tune custom mod"(this part is always different, sometimes it plays until the end, sometimes it even ends before this). I feel that this certainly is a bug/issue that needs to be fixed if my implementation is not wrong.

I would be really grateful if you could look into this. I will also be playing around the library and comment if I find anything. Thank you so much!

ryanheise commented 8 months ago

I will be disposing the api-key once this issue will be resolved.

Can you reproduce this by downloading the response beforehand, saving it to a file, putting it in the assets directory and changing your stream audio source to read from that file stream?

JakeSeo commented 8 months ago

I haven't tried, but i think it will work fine if i download the file. I tested this in Postman and it was ok.

JakeSeo commented 7 months ago

In, iOS, the first few parts are lost.

yadaniyil commented 2 weeks ago

Same for me. On iOS audio playback skips the first 3-4 words. @JakeSeo did you find the solution?