just_audio icon indicating copy to clipboard operation
just_audio copied to clipboard

Proxy Handler triggered two times in StreamAudioSource

Open JakeSeo opened this issue 1 year ago • 8 comments

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

  • if the stream is not a broadcast stream, it throws bad state error since the listener is registered two times.

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):

  • OS: -
  • Browser -

Smartphone (please complete the following information):

  • Device: Galaxy S23 FE
  • OS: Android 14

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

JakeSeo avatar Feb 22 '24 08:02 JakeSeo