Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update from main - there is 0.85.3 changelog section added. This PR goes to 0.85.3.

* 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
Expand Down
1 change: 1 addition & 0 deletions sdk/python/packages/flet-audio/src/flet_audio/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 39 additions & 14 deletions sdk/python/packages/flet-audio/src/flet_audio/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -136,12 +130,42 @@ 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<void> _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<dynamic> _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.
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();
Expand All @@ -158,6 +182,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;
Expand Down
Loading