From 17b8e6b3ea5fe73032b948741f94baa217af79d6 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sat, 30 May 2026 01:06:15 +0200 Subject: [PATCH 1/3] initial commit --- .../flet-audio/src/flet_audio/audio.py | 1 + .../src/flutter/flet_audio/lib/src/audio.dart | 50 +++++++++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/sdk/python/packages/flet-audio/src/flet_audio/audio.py b/sdk/python/packages/flet-audio/src/flet_audio/audio.py index 5fdc039ae2..75208d1950 100644 --- a/sdk/python/packages/flet-audio/src/flet_audio/audio.py +++ b/sdk/python/packages/flet-audio/src/flet_audio/audio.py @@ -107,6 +107,7 @@ async def play(self, position: ft.DurationValue = 0): Args: position: The position to start playback from. + Defaults to the beginning of the audio. """ await self._invoke_method( method_name="play", diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart index fed3367b1b..44751b6a60 100644 --- a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart @@ -85,20 +85,14 @@ class AudioService extends FletService { _src = resolvedSrc.uri; _srcBytes = null; srcChanged = true; - - var assetSrc = control.backend.getAssetSource(_src!); - if (assetSrc.isFile) { - await player.setSourceDeviceFile(assetSrc.path); - } else { - await player.setSourceUrl(assetSrc.path); - } + await _applySource(); } else if (resolvedSrc.bytes != null && (_srcBytes == null || !listEquals(_srcBytes, resolvedSrc.bytes))) { // bytes _srcBytes = resolvedSrc.bytes; _src = null; srcChanged = true; - await player.setSourceBytes(resolvedSrc.bytes!); + await _applySource(); } if (srcChanged) { @@ -136,12 +130,44 @@ class AudioService extends FletService { }(); } + /// Pushes the currently tracked source ([_src] or [_srcBytes]) to the native + /// [player], preparing it for playback. + /// + /// Used both when the source changes (from [update]) and to re-prepare + /// playback after the player has reached [PlayerState.completed] under + /// [ReleaseMode.release], where the native source has already been freed. + Future _applySource() async { + if (_src != null) { + final assetSrc = control.backend.getAssetSource(_src!); + if (assetSrc.isFile) { + await player.setSourceDeviceFile(assetSrc.path); + } else { + await player.setSourceUrl(assetSrc.path); + } + } else if (_srcBytes != null) { + await player.setSourceBytes(_srcBytes!); + } + } + Future _invokeMethod(String name, dynamic args) async { debugPrint("Audio.$name($args)"); switch (name) { case "play": final position = parseDuration(args["position"]); - if (position != null) { + if (player.state == PlayerState.completed) { + // Playback finished. Under ReleaseMode.release the native source has + // been freed, so re-prepare it before resuming: seeking/resuming a + // freed source hangs forever (the player never emits onSeekComplete, + // surfacing on the Python side as a 30s TimeoutException). See #6536. + if ((_releaseMode ?? ReleaseMode.release) == ReleaseMode.release) { + await _applySource(); + } + // Position is already reset to the start after completion, so only + // seek when a specific non-zero position was requested. + if (position != null && position > Duration.zero) { + await player.seek(position); + } + } else if (position != null) { await player.seek(position); } await player.resume(); @@ -158,6 +184,12 @@ class AudioService extends FletService { case "seek": final position = parseDuration(args["position"]); if (position != null) { + if (player.state == PlayerState.completed && + (_releaseMode ?? ReleaseMode.release) == ReleaseMode.release) { + // Source was freed on completion (see "play"); re-prepare it, + // otherwise the seek would hang waiting for onSeekComplete. + await _applySource(); + } await player.seek(position); } break; From 730a23bc079f640f53bd6f6dfffe11961cfd4ca4 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sat, 30 May 2026 01:06:41 +0200 Subject: [PATCH 2/3] improve `ReleaseMode` docs --- .../flet-audio/src/flet_audio/types.py | 53 ++++++++++++++----- .../src/flutter/flet_audio/lib/src/audio.dart | 4 +- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/sdk/python/packages/flet-audio/src/flet_audio/types.py b/sdk/python/packages/flet-audio/src/flet_audio/types.py index bf101549d4..ad534910be 100644 --- a/sdk/python/packages/flet-audio/src/flet_audio/types.py +++ b/sdk/python/packages/flet-audio/src/flet_audio/types.py @@ -17,31 +17,56 @@ class ReleaseMode(Enum): - """The behavior of Audio player when an audio is finished or stopped.""" + """ + Determines what happens to the player's resources (the buffered audio and + the underlying native player) when playback finishes or is stopped, and + therefore how quickly, and at what cost, the same audio can be played again. + """ RELEASE = "release" """ - Releases all resources, just like calling release method. - - Info: - - On Android, the media player is quite resource-intensive, and this will - let it go. Data will be buffered again when needed (if it's a remote file, - it will be downloaded again). - - On iOS and macOS, works just like - :meth:`flet_audio.Audio.release` method. + Frees all resources once playback completes, as if + :meth:`flet_audio.Audio.release` had been called automatically. + This is the default mode. + + The buffered audio and the native player are released, so nothing is kept in + memory while the audio is idle. Playing again is still supported, but the + source is **loaded again from scratch** first (and a remote file is + re-downloaded), which adds a short delay before playback starts. + + Best when replays are rare or may never happen and keeping memory usage to a + minimum matters. + + Note: + - On Android, the native media player is resource-intensive; this mode + lets it go and re-buffers the data only when needed. + - On iOS and macOS, behaves like :meth:`flet_audio.Audio.release`. """ LOOP = "loop" """ - Keeps buffered data and plays again after completion, creating a loop. - Notice that calling stop method is not enough to release the resources - when this mode is being used. + Automatically restarts playback from the beginning every time it completes, + creating a continuous loop. All resources are kept buffered. + + Best for audio that should repeat indefinitely, such as background music. + + Note: + Resources are never released automatically in this mode. To free them, + change the source or call :meth:`flet_audio.Audio.release`. """ STOP = "stop" """ - Stops audio playback but keep all resources intact. - Use this if you intend to play again later. + Stops playback when the audio completes but keeps all resources (the + buffered audio and the native player) intact. + + Because nothing is released, playing again is **immediate**: the audio + restarts straight from the retained buffer, with no reloading or + re-downloading. The trade-off is that these resources stay in memory while + the audio is idle. + + Best when you intend to replay the same audio and want instant, + network-free playback (for example, short sound effects or repeated tones). """ diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart index 44751b6a60..973d2e31f7 100644 --- a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart @@ -156,9 +156,7 @@ class AudioService extends FletService { final position = parseDuration(args["position"]); if (player.state == PlayerState.completed) { // Playback finished. Under ReleaseMode.release the native source has - // been freed, so re-prepare it before resuming: seeking/resuming a - // freed source hangs forever (the player never emits onSeekComplete, - // surfacing on the Python side as a 30s TimeoutException). See #6536. + // been freed, so re-prepare it before resuming. if ((_releaseMode ?? ReleaseMode.release) == ReleaseMode.release) { await _applySource(); } From 7b1b74f4c6555a88e71702d674d88f6cb64ef210 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sat, 30 May 2026 01:21:58 +0200 Subject: [PATCH 3/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1d96031d..d455f023fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Bug fixes +* Fix `flet-audio.Audio.play()`/`seek()` timing out when replaying after playback had completed: under the default `ReleaseMode.RELEASE` the source is freed on completion and is now re-prepared on replay ([#6536](https://github.com/flet-dev/flet/issues/6536), [#6538](https://github.com/flet-dev/flet/pull/6538)) by @ndonkoHenri. * Fix cross-tab session contamination on Flet web: opening the same app URL in a duplicated browser tab no longer steals the original tab's output connection via `sessionStorage`-cloned `_flet_session_id`. `REGISTER_CLIENT` now rejects session reuse when the existing session still has a live `connection`, allocating a fresh session for the second tab while preserving legitimate single-tab reconnect after refresh or network blip (where `connection` is already `None`) ([#6512](https://github.com/flet-dev/flet/issues/6512), [#6513](https://github.com/flet-dev/flet/pull/6513)) by @ihmily. ## 0.85.1