From 52b0f0ad8ec122228d9bb423024c6cc9bfc3ced3 Mon Sep 17 00:00:00 2001 From: syle Date: Thu, 12 Dec 2024 16:28:27 -0600 Subject: [PATCH 01/12] audio_service addon to flet_video --- .../android/app/src/main/AndroidManifest.xml | 28 ++- packages/flet_video/lib/src/video.dart | 201 +++++++++++++++++- packages/flet_video/pubspec.yaml | 2 + .../packages/flet/src/flet/core/video.py | 12 ++ 4 files changed, 233 insertions(+), 10 deletions(-) diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index 29ebfbb35..04dd59fd9 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + @@ -28,7 +32,7 @@ android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/> + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index f2f886926..f8f5f6ae1 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -5,6 +5,9 @@ import 'package:flet/flet.dart'; import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:rxdart/rxdart.dart'; + import 'utils/video.dart'; @@ -24,6 +27,8 @@ class VideoControl extends StatefulWidget { } class _VideoControlState extends State with FletStoreMixin { + int _lastProcessedIndex = -1; + int _lastPercent = -1; late final playerConfig = PlayerConfiguration( title: widget.control.attrString("title", "Flet Video")!, muted: widget.control.attrBool("muted", false)!, @@ -38,16 +43,35 @@ class _VideoControlState extends State with FletStoreMixin { late final Player player = Player( configuration: playerConfig, ); + late final AudioHandler _audioHandler; + late final videoControllerConfiguration = parseControllerConfiguration( widget.control, "configuration", const VideoControllerConfiguration())!; late final controller = VideoController(player, configuration: videoControllerConfiguration); + @override void initState() { super.initState(); + + AudioService.init( + builder: () => MyAudioHandler(player), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.appveyor.flet.channel.audio', + androidNotificationChannelName: 'Audio playback', + androidNotificationOngoing: true, + ), + ).then((handler) { + _audioHandler = handler; + // Now you can use _audioHandler for audio service operations + }).catchError((error) { + // Handle errors during initialization + debugPrint('Error initializing AudioService: $error'); + }); player.open(Playlist(parseVideoMedia(widget.control, "playlist")), play: widget.control.attrBool("autoPlay", false)!); + } @override @@ -74,6 +98,13 @@ class _VideoControlState extends State with FletStoreMixin { .triggerControlEvent(widget.control.id, "track_changed", message ?? ""); } + void _onPercentChanged(String? message) { + // Let's not debug print this, cause to much traffic on console + // debugPrint("Video onPercentChanged: $message"); + widget.backend + .triggerControlEvent(widget.control.id, "percent_changed", message ?? ""); + } + @override Widget build(BuildContext context) { debugPrint("Video build: ${widget.control.id}"); @@ -105,6 +136,7 @@ class _VideoControlState extends State with FletStoreMixin { bool onError = widget.control.attrBool("onError", false)!; bool onCompleted = widget.control.attrBool("onCompleted", false)!; bool onTrackChanged = widget.control.attrBool("onTrackChanged", false)!; + bool onPercentChanged = widget.control.attrBool("onPercentChanged", false)!; double? volume = widget.control.attrDouble("volume"); double? pitch = widget.control.attrDouble("pitch"); @@ -200,25 +232,25 @@ class _VideoControlState extends State with FletStoreMixin { switch (methodName) { case "play": debugPrint("Video.play($hashCode)"); - await player.play(); + await _audioHandler.play(); break; case "pause": debugPrint("Video.pause($hashCode)"); - await player.pause(); + await _audioHandler.pause(); break; case "play_or_pause": debugPrint("Video.playOrPause($hashCode)"); - await player.playOrPause(); + await _audioHandler.play(); break; case "stop": debugPrint("Video.stop($hashCode)"); - await player.stop(); + await _audioHandler.stop(); player.open(Playlist(parseVideoMedia(widget.control, "playlist")), play: false); break; case "seek": debugPrint("Video.jump($hashCode)"); - await player.seek(Duration( + await _audioHandler.seek(Duration( milliseconds: int.tryParse(args["position"] ?? "") ?? 0)); break; case "next": @@ -278,14 +310,169 @@ class _VideoControlState extends State with FletStoreMixin { _onCompleted(event.toString()); } }); + // Send percentage position change between 0-100 to use with flet slider. + // This will make flet event loop less busy sending round int numbers + // as well as throttling to 1 second to not overload flet socket + player.stream.position.throttleTime(const Duration(seconds: 1)).listen((position) { + if (onPercentChanged) { + try { + final int percent = (position.inMilliseconds / player.state.duration.inMilliseconds * 100).toInt(); + if (percent != _lastPercent) { + _lastPercent = percent; + _onPercentChanged(percent.toString()); + } + } catch (e) { + debugPrint("Error calculating percentage: $e"); + } + } + }); player.stream.playlist.listen((event) { - if (onTrackChanged) { - _onTrackChanged(event.index.toString()); + if (event.index != _lastProcessedIndex) { // prevent duplicates + _lastProcessedIndex = event.index; + if (onTrackChanged) { + _onTrackChanged(event.index.toString()); + } + // We want this outside of onTrackChanged as we need to update + // notification bar regardless if they subscribed to handler or not. + _audioHandler.customAction('update_notification', {'index': event.index}); } }); return constrainedControl(context, video, widget.parent, widget.control); }); } +} + +class MyAudioHandler extends BaseAudioHandler { + final Player player; + MyAudioHandler(this.player); + final PlaybackState _basePlaybackState = PlaybackState( + controls: [ + MediaControl.skipToPrevious, + MediaControl.play, + MediaControl.pause, + MediaControl.skipToNext, + ], + systemActions: const { + MediaAction.seek, + }, + processingState: AudioProcessingState.ready, + ); + + @override + Future customAction(String name, + [Map? extras]) async { + if (name == 'update_notification') { + final index = extras?['index'] as int?; + if (index != null) { + updatePlaybackState(index); + } + } else { + debugPrint("Unknown custom action: $name"); + } + } + + @override + Future play() async { + try { + await player.playOrPause(); + // we need to trigger updatePlaybackState if first time hitting play + // or seek on notification bar will not work correctly + if (player.state.position.inMilliseconds == 0) { + updatePlaybackState(player.state.playlist.index); + } else { + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: player.state.position)); + } + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + + @override + Future pause() async { + try { + await player.playOrPause(); + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: player.state.position)); + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + + @override + Future stop() async { + try { + await player.stop(); + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: player.state.position)); + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + // Because these 2 functions are changing songs, no need to call + // playbackState as track change will trigger player.stream.playlist.listen + // to call updatePlaybackState() to do it for us, still required for + // bluetooth etc devices to work correctly + @override + Future skipToNext() async { + try { + await player.next(); + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + + @override + Future skipToPrevious() async { + try { + await player.previous(); + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + + @override + Future seek(Duration position) async { + try { + await player.seek(position); + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: position)); + } catch (e) { + debugPrint("Playback error: ${e}"); + } + } + + void updatePlaybackState(int index) { + final currentMedia = player.state.playlist.medias[index]; + final extras = currentMedia.extras; + // Url Decode and extract filename + // I am going to assume structure of https://blah.com/Artist - SongName.mp3 + // Let's attempt to split their filename into title/artist if they did not + // include extras { title and artist } as full http url would be ugly + final filename = Uri.decodeFull(player.state.playlist.medias[index].uri.split('/').last); + final parts = filename.split('.'); + final filenameWithoutExtension = parts.sublist(0, parts.length - 1).join('.'); + final artistAndTitle = filenameWithoutExtension.split('-'); + final title = extras?['title'] ?? artistAndTitle.last.trim(); + final artist = extras?['artist'] ?? artistAndTitle.sublist(0, artistAndTitle.length - 1).join('-').trim(); + final artUri = extras?['artUri'] as String?; + final album = extras?['album'] as String?; + final genre = extras?['genre'] as String?; + final displayTitle = extras?['displayTitle'] as String?; + final displaySubtitle = extras?['displaySubtitle'] as String?; + final displayDescription = extras?['displayDescription'] as String?; + + mediaItem.add(MediaItem( + id: index.toString(), + title: title, + artist: artist, + artUri: artUri != null ? Uri.parse(artUri) : null, + album: album != null ? album : null, + genre: genre != null ? genre : null, + displayTitle: displayTitle != null ? displayTitle : null, + displaySubtitle: displaySubtitle != null ? displaySubtitle : null, + displayDescription: displayDescription != null ? displayDescription : null, + duration: player.state.duration, + )); + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: Duration.zero)); + debugPrint("Now playing: ${player.state.playlist.medias[index].uri} with index ${player.state.playlist.index}"); + } } \ No newline at end of file diff --git a/packages/flet_video/pubspec.yaml b/packages/flet_video/pubspec.yaml index 95d0e479d..1c6c6660b 100644 --- a/packages/flet_video/pubspec.yaml +++ b/packages/flet_video/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: media_kit: ^1.1.10 media_kit_video: ^1.2.4 media_kit_libs_video: ^1.0.4 + audio_service: ^0.18.15 + rxdart: ^0.28.0 flet: path: ../flet/ diff --git a/sdk/python/packages/flet/src/flet/core/video.py b/sdk/python/packages/flet/src/flet/core/video.py index 5f93717df..f06a2ce42 100644 --- a/sdk/python/packages/flet/src/flet/core/video.py +++ b/sdk/python/packages/flet/src/flet/core/video.py @@ -96,6 +96,7 @@ def __init__( on_error: OptionalControlEventCallable = None, on_completed: OptionalControlEventCallable = None, on_track_changed: OptionalControlEventCallable = None, + on_percent_changed: OptionalControlEventCallable = None, # # ConstrainedControl # @@ -182,6 +183,7 @@ def __init__( self.on_error = on_error self.on_completed = on_completed self.on_track_changed = on_track_changed + self.on_percent_changed = on_percent_changed def _get_control_name(self): return "video" @@ -550,3 +552,13 @@ def on_track_changed(self) -> OptionalControlEventCallable: def on_track_changed(self, handler: OptionalControlEventCallable): self._set_attr("onTrackChanged", True if handler is not None else None) self._add_event_handler("track_changed", handler) + + # on_pos_changed + @property + def on_percent_changed(self) -> OptionalControlEventCallable: + return self._get_event_handler("percent_changed") + + @on_percent_changed.setter + def on_percent_changed(self, handler: OptionalControlEventCallable): + self._set_attr("onPercentChanged", True if handler is not None else None) + self._add_event_handler("percent_changed", handler) From 8d93d6e969f9b785a7712e9099c4b8642bdc50ad Mon Sep 17 00:00:00 2001 From: syle Date: Fri, 13 Dec 2024 08:01:08 -0600 Subject: [PATCH 02/12] Change on_percent to on_position - send back position,duration,percent - throttle configurable --- packages/flet_video/lib/src/video.dart | 37 ++++++++------- .../packages/flet/src/flet/core/video.py | 46 +++++++++++++++---- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index f8f5f6ae1..b84f487cb 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -28,7 +28,7 @@ class VideoControl extends StatefulWidget { class _VideoControlState extends State with FletStoreMixin { int _lastProcessedIndex = -1; - int _lastPercent = -1; + Duration _lastEmittedPosition = Duration.zero; late final playerConfig = PlayerConfiguration( title: widget.control.attrString("title", "Flet Video")!, muted: widget.control.attrBool("muted", false)!, @@ -98,13 +98,18 @@ class _VideoControlState extends State with FletStoreMixin { .triggerControlEvent(widget.control.id, "track_changed", message ?? ""); } - void _onPercentChanged(String? message) { - // Let's not debug print this, cause to much traffic on console - // debugPrint("Video onPercentChanged: $message"); - widget.backend - .triggerControlEvent(widget.control.id, "percent_changed", message ?? ""); + void _onPositionChanged(Duration position, Duration duration, int percent) { + // commenting out, may be too verbose to display every 1 second + // debugPrint("New Position is ${position} and duration is ${duration} and percent is ${percent}"); + final data = { + "position": position.inSeconds, // Send position in seconds + "duration": duration.inSeconds, // Send duration in seconds + "percent": percent, + }; + widget.backend.triggerControlEvent(widget.control.id, "positionChanged", jsonEncode(data)); } + @override Widget build(BuildContext context) { debugPrint("Video build: ${widget.control.id}"); @@ -136,7 +141,9 @@ class _VideoControlState extends State with FletStoreMixin { bool onError = widget.control.attrBool("onError", false)!; bool onCompleted = widget.control.attrBool("onCompleted", false)!; bool onTrackChanged = widget.control.attrBool("onTrackChanged", false)!; - bool onPercentChanged = widget.control.attrBool("onPercentChanged", false)!; + bool onPositionChanged = widget.control.attrBool("onPositionChanged", false)!; + int throttle = widget.control.attrInt("throttle", 1000)!; + double? volume = widget.control.attrDouble("volume"); double? pitch = widget.control.attrDouble("pitch"); @@ -311,18 +318,16 @@ class _VideoControlState extends State with FletStoreMixin { } }); // Send percentage position change between 0-100 to use with flet slider. - // This will make flet event loop less busy sending round int numbers // as well as throttling to 1 second to not overload flet socket - player.stream.position.throttleTime(const Duration(seconds: 1)).listen((position) { - if (onPercentChanged) { + player.stream.position.throttleTime(Duration(milliseconds: throttle)).listen((position) { + if (onPositionChanged && position.inSeconds != _lastEmittedPosition.inSeconds) { try { - final int percent = (position.inMilliseconds / player.state.duration.inMilliseconds * 100).toInt(); - if (percent != _lastPercent) { - _lastPercent = percent; - _onPercentChanged(percent.toString()); - } + final duration = player.state.duration; + final int percent = (position.inMilliseconds / duration.inMilliseconds * 100).toInt(); + _lastEmittedPosition = position; + _onPositionChanged(position, duration, percent); } catch (e) { - debugPrint("Error calculating percentage: $e"); + debugPrint("Error in OnPositionChanged: $e"); } } }); diff --git a/sdk/python/packages/flet/src/flet/core/video.py b/sdk/python/packages/flet/src/flet/core/video.py index f06a2ce42..1d388eb9c 100644 --- a/sdk/python/packages/flet/src/flet/core/video.py +++ b/sdk/python/packages/flet/src/flet/core/video.py @@ -1,4 +1,5 @@ import dataclasses +import json from enum import Enum from typing import Any, Dict, List, Optional, Union, cast @@ -8,6 +9,8 @@ from flet.core.box import FilterQuality from flet.core.constrained_control import ConstrainedControl from flet.core.control import OptionalNumber +from flet.core.control_event import ControlEvent +from flet.core.event_handler import EventHandler from flet.core.ref import Ref from flet.core.text_style import TextStyle from flet.core.tooltip import TooltipValue @@ -58,6 +61,14 @@ class VideoSubtitleConfiguration: padding: Optional[PaddingValue] = dataclasses.field(default=None) visible: Optional[bool] = dataclasses.field(default=None) +class VideoPositionChangedEvent(ControlEvent): + def __init__(self, e: ControlEvent): + super().__init__(e.target, e.name, e.data, e.control, e.page) + d = json.loads(e.data) + self.position: int = d.get("position") + self.duration: int = d.get("duration") + self.percent: int = d.get("percent") + class Video(ConstrainedControl): """ @@ -81,6 +92,7 @@ def __init__( playlist_mode: Optional[PlaylistMode] = None, shuffle_playlist: Optional[bool] = None, volume: OptionalNumber = None, + throttle: Optional[int] = None, playback_rate: OptionalNumber = None, alignment: Optional[Alignment] = None, filter_quality: Optional[FilterQuality] = None, @@ -96,7 +108,7 @@ def __init__( on_error: OptionalControlEventCallable = None, on_completed: OptionalControlEventCallable = None, on_track_changed: OptionalControlEventCallable = None, - on_percent_changed: OptionalControlEventCallable = None, + on_position_changed: OptionalControlEventCallable = None, # # ConstrainedControl # @@ -165,6 +177,7 @@ def __init__( self.pitch = pitch self.fill_color = fill_color self.volume = volume + self.throttle = throttle self.playback_rate = playback_rate self.alignment = alignment self.wakelock = wakelock @@ -183,7 +196,13 @@ def __init__( self.on_error = on_error self.on_completed = on_completed self.on_track_changed = on_track_changed - self.on_percent_changed = on_percent_changed + self.__on_position_changed = EventHandler( + lambda e: VideoPositionChangedEvent(e) + ) + self._add_event_handler( + "positionChanged", self.__on_position_changed.get_handler() + ) + self.on_position_changed = on_position_changed def _get_control_name(self): return "video" @@ -418,6 +437,15 @@ def volume(self, value: OptionalNumber): assert value is None or 0 <= value <= 100, "volume must be between 0 and 100" self._set_attr("volume", value) + # throttle + @property + def throttle(self) -> Optional[int]: + return self._get_attr("throttle", data_type="int") + + @throttle.setter + def throttle(self, value: Optional[int]): + self._set_attr("throttle", value) + # playback_rate @property def playback_rate(self) -> OptionalNumber: @@ -553,12 +581,12 @@ def on_track_changed(self, handler: OptionalControlEventCallable): self._set_attr("onTrackChanged", True if handler is not None else None) self._add_event_handler("track_changed", handler) - # on_pos_changed + # on_position_changed @property - def on_percent_changed(self) -> OptionalControlEventCallable: - return self._get_event_handler("percent_changed") + def on_position_changed(self,) -> OptionalEventCallable[VideoPositionChangedEvent]: + return self.__on_position_changed.handler - @on_percent_changed.setter - def on_percent_changed(self, handler: OptionalControlEventCallable): - self._set_attr("onPercentChanged", True if handler is not None else None) - self._add_event_handler("percent_changed", handler) + @on_position_changed.setter + def on_position_changed(self, handler: OptionalEventCallable[VideoPositionChangedEvent]): + self.__on_position_changed.handler = handler + self._set_attr("onPositionChanged", True if handler is not None else None) From 651688639eaac911a569f0132c79cd3602321928 Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:28:39 +0100 Subject: [PATCH 03/12] fix broken `Map.center_on()` and default animations (#4519) * fix center_on * get default animation duration and curve --- packages/flet_map/lib/src/map.dart | 8 +++++--- sdk/python/packages/flet/src/flet/core/map/map.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/flet_map/lib/src/map.dart b/packages/flet_map/lib/src/map.dart index f82db9b75..73c7e0ca3 100644 --- a/packages/flet_map/lib/src/map.dart +++ b/packages/flet_map/lib/src/map.dart @@ -54,8 +54,10 @@ class _MapControlState extends State .where((c) => c.isVisible && (acceptedChildrenTypes.contains(c.type))) .toList(); - Curve? defaultAnimationCurve; - Duration? defaultAnimationDuration; + Curve? defaultAnimationCurve = + parseCurve(widget.control.attrString("animationCurve")); + Duration? defaultAnimationDuration = + parseDuration(widget.control, "animationDuration"); var configuration = parseConfiguration( widget.control, widget.backend, context, const MapOptions())!; @@ -76,7 +78,7 @@ class _MapControlState extends State if (degree != null) { _animatedMapController.animatedRotateFrom( degree, - curve: parseCurve(args["curve"]) ?? defaultAnimationCurve, + curve: parseCurve(args["curve"], defaultAnimationCurve), ); } case "reset_rotation": diff --git a/sdk/python/packages/flet/src/flet/core/map/map.py b/sdk/python/packages/flet/src/flet/core/map/map.py index 05410cd68..34706ff19 100644 --- a/sdk/python/packages/flet/src/flet/core/map/map.py +++ b/sdk/python/packages/flet/src/flet/core/map/map.py @@ -366,7 +366,7 @@ def center_on( animation_duration: DurationValue = None, ): self.invoke_method( - "animate_to", + "center_on", arguments={ "lat": str(point.latitude) if point else None, "long": str(point.longitude) if point else None, From 88e2adc56735442c75d6db6f20c06a1d1bcd318c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 12 Dec 2024 14:30:07 -0800 Subject: [PATCH 04/12] Fix CI in `main` branch --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 5638e4dda..c952f9098 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -414,7 +414,7 @@ for: install: - . ci/common.sh - - flutter upgrade --force + - source ci/install_flutter.sh build_script: # Flutter Web client From 5b47aa668e0a0b7abd1f6d88dbdf423849f7315e Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:32:55 +0100 Subject: [PATCH 05/12] fix: Tooltip corruption in `Segment` and `BarChartRod` on `update()` (#4525) * avoid jsonDecoding `Segment` and `BarChartRod` tooltips * avoid jsonEncoding `Segment` and `BarChartRod` tooltips * Unset theme visual density default * Unset `SegmentedButton` border side default * `TextField.hint_text` should be displayed if `label` is not specified --- packages/flet/lib/src/controls/barchart.dart | 5 ++--- packages/flet/lib/src/controls/segmented_button.dart | 5 +---- packages/flet/lib/src/utils/form_field.dart | 4 +++- .../packages/flet/src/flet/core/charts/bar_chart_rod.py | 2 +- sdk/python/packages/flet/src/flet/core/control.py | 6 ++++-- sdk/python/packages/flet/src/flet/core/theme.py | 4 +--- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/flet/lib/src/controls/barchart.dart b/packages/flet/lib/src/controls/barchart.dart index f3f3f2e01..ae585a451 100644 --- a/packages/flet/lib/src/controls/barchart.dart +++ b/packages/flet/lib/src/controls/barchart.dart @@ -263,9 +263,8 @@ class _BarChartControlState extends State { getTooltipItem: (group, groupIndex, rod, rodIndex) { var dp = viewModel.barGroups[groupIndex].barRods[rodIndex]; - var tooltip = dp.control.attrString("tooltip") != null - ? jsonDecode(dp.control.attrString("tooltip")!) - : dp.control.attrDouble("toY", 0)!.toString(); + var tooltip = dp.control.attrString("tooltip", + dp.control.attrDouble("toY", 0)!.toString())!; var tooltipStyle = parseTextStyle( Theme.of(context), dp.control, "tooltipStyle"); tooltipStyle ??= const TextStyle(); diff --git a/packages/flet/lib/src/controls/segmented_button.dart b/packages/flet/lib/src/controls/segmented_button.dart index 35763d530..f838c9288 100644 --- a/packages/flet/lib/src/controls/segmented_button.dart +++ b/packages/flet/lib/src/controls/segmented_button.dart @@ -49,7 +49,6 @@ class _SegmentedButtonControlState extends State defaultSurfaceTintColor: theme.colorScheme.surfaceTint, defaultElevation: 1, defaultPadding: const EdgeInsets.symmetric(horizontal: 8), - defaultBorderSide: BorderSide.none, defaultShape: theme.useMaterial3 ? const StadiumBorder() : RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))); @@ -124,9 +123,7 @@ class _SegmentedButtonControlState extends State return ButtonSegment( value: segmentView.control.attrString("value")!, enabled: !segmentDisabled, - tooltip: !segmentDisabled && segmentTooltip != null - ? jsonDecode(segmentTooltip) - : null, + tooltip: segmentDisabled ? null : segmentTooltip, icon: iconCtrls.isNotEmpty ? createControl(segmentView.control, iconCtrls.first.id, segmentDisabled) diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index 720dc5816..b476ee1f1 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -154,7 +154,9 @@ InputDecoration buildInputDecoration(BuildContext context, Control control, label: label != null ? createControl(control, label.id, control.isDisabled, parentAdaptive: adaptive) - : Text(control.attrString("label", "")!), + : control.attrString("label") != null + ? Text(control.attrString("label")!) + : null, labelStyle: parseTextStyle(Theme.of(context), control, "labelStyle"), border: border, enabledBorder: border, diff --git a/sdk/python/packages/flet/src/flet/core/charts/bar_chart_rod.py b/sdk/python/packages/flet/src/flet/core/charts/bar_chart_rod.py index d0728f25f..48c99f724 100644 --- a/sdk/python/packages/flet/src/flet/core/charts/bar_chart_rod.py +++ b/sdk/python/packages/flet/src/flet/core/charts/bar_chart_rod.py @@ -68,7 +68,7 @@ def __init__( self.tooltip_style = tooltip_style def _get_control_name(self): - return "rod" + return "bar_chart_rod" def before_update(self): super().before_update() diff --git a/sdk/python/packages/flet/src/flet/core/control.py b/sdk/python/packages/flet/src/flet/core/control.py index a7bd44323..46c392236 100644 --- a/sdk/python/packages/flet/src/flet/core/control.py +++ b/sdk/python/packages/flet/src/flet/core/control.py @@ -91,10 +91,12 @@ def before_update(self): pass def _before_build_command(self) -> None: - self._set_attr_json("col", self.__col) - self._set_attr_json("tooltip", self.tooltip) + if self._get_control_name() not in ["segment", "bar_chart_rod"]: + # see https://github.com/flet-dev/flet/pull/4525 + self._set_attr_json("tooltip", self.tooltip) if isinstance(self.badge, (Badge, str)): self._set_attr_json("badge", self.badge) + self._set_attr_json("col", self.__col) def did_mount(self): pass diff --git a/sdk/python/packages/flet/src/flet/core/theme.py b/sdk/python/packages/flet/src/flet/core/theme.py index e9a0711d4..f8ce4b0b8 100644 --- a/sdk/python/packages/flet/src/flet/core/theme.py +++ b/sdk/python/packages/flet/src/flet/core/theme.py @@ -888,6 +888,4 @@ class Theme: text_theme: Optional[TextTheme] = None time_picker_theme: Optional[TimePickerTheme] = None tooltip_theme: Optional[TooltipTheme] = None - visual_density: Union[VisualDensity, ThemeVisualDensity] = field( - default=VisualDensity.STANDARD - ) + visual_density: Union[VisualDensity, ThemeVisualDensity] = None From 6b8b9c01bda1186bfd797a7ac191d6ee066e55da Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:33:35 +0100 Subject: [PATCH 06/12] fix: Setting `CheckBox.border_side.stroke_align` to an Enum fails (#4526) * `BorderSideStrokeAlign` should inherit from float * properly parse `Chip.border_side` --- sdk/python/packages/flet/src/flet/core/border.py | 16 ++++++++-------- sdk/python/packages/flet/src/flet/core/chip.py | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/core/border.py b/sdk/python/packages/flet/src/flet/core/border.py index 3f01866e5..74d20c218 100644 --- a/sdk/python/packages/flet/src/flet/core/border.py +++ b/sdk/python/packages/flet/src/flet/core/border.py @@ -1,11 +1,11 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum from typing import Optional, Union from flet.core.types import ColorValue, OptionalNumber -class BorderSideStrokeAlign(Enum): +class BorderSideStrokeAlign(float, Enum): INSIDE = -1.0 CENTER = 0.0 OUTSIDE = 1.0 @@ -14,16 +14,16 @@ class BorderSideStrokeAlign(Enum): @dataclass class BorderSide: width: OptionalNumber - color: Optional[ColorValue] = field(default=None) - stroke_align: Union[BorderSideStrokeAlign, OptionalNumber] = field(default=None) + color: Optional[ColorValue] = None + stroke_align: Union[BorderSideStrokeAlign, OptionalNumber] = None @dataclass class Border: - top: Optional[BorderSide] = field(default=None) - right: Optional[BorderSide] = field(default=None) - bottom: Optional[BorderSide] = field(default=None) - left: Optional[BorderSide] = field(default=None) + top: Optional[BorderSide] = None + right: Optional[BorderSide] = None + bottom: Optional[BorderSide] = None + left: Optional[BorderSide] = None def all(width: Optional[float] = None, color: Optional[ColorValue] = None) -> Border: diff --git a/sdk/python/packages/flet/src/flet/core/chip.py b/sdk/python/packages/flet/src/flet/core/chip.py index 6d6a5626b..8a909277e 100644 --- a/sdk/python/packages/flet/src/flet/core/chip.py +++ b/sdk/python/packages/flet/src/flet/core/chip.py @@ -210,6 +210,7 @@ def before_update(self): self._set_attr_json("labelStyle", self.__label_style) self._set_attr_json("padding", self.__padding) self._set_attr_json("shape", self.__shape) + self._set_attr_json("borderSide", self.__border_side) self._set_attr_json("color", self.__color, wrap_attr_dict=True) def _get_children(self): From ec2f76ce371be8519cbaeef672e43b1953e1b182 Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:35:23 +0100 Subject: [PATCH 07/12] fix:`ControlState` should be resolved based on user-defined order (#4556) * ControlState: rename "" to "default" * resolve ControlState on user-defined order * fix failing tests * remove breaking line --- .../flet/lib/src/utils/material_state.dart | 42 +++++++++---------- .../flet/src/flet/core/popup_menu_button.py | 1 - .../packages/flet/src/flet/core/types.py | 2 +- .../packages/flet/tests/test_datatable.py | 15 ++++--- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/flet/lib/src/utils/material_state.dart b/packages/flet/lib/src/utils/material_state.dart index c6c19dbc4..564a9ebf9 100644 --- a/packages/flet/lib/src/utils/material_state.dart +++ b/packages/flet/lib/src/utils/material_state.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'package:flutter/material.dart'; WidgetStateProperty? getWidgetStateProperty( @@ -8,43 +9,42 @@ WidgetStateProperty? getWidgetStateProperty( } var j = jsonDictValue; if (j is! Map) { - j = {"": j}; + j = {"default": j}; } return WidgetStateFromJSON(j, converterFromJson, defaultValue); } class WidgetStateFromJSON extends WidgetStateProperty { - late final Map _states; + late final LinkedHashMap _states; late final T? _defaultValue; WidgetStateFromJSON(Map? jsonDictValue, T Function(dynamic) converterFromJson, T? defaultValue) { _defaultValue = defaultValue; - _states = {}; - if (jsonDictValue != null) { - jsonDictValue.forEach((stateStr, jv) { - stateStr.split(",").map((s) => s.trim().toLowerCase()).forEach((state) { - _states[state] = converterFromJson(jv); - }); - }); - } + + // preserve user-defined order + _states = LinkedHashMap.from( + jsonDictValue?.map((key, value) { + var normalizedKey = key.trim().toLowerCase(); + // "" is now deprecated; use "default" instead + if (normalizedKey == "") normalizedKey = "default"; + return MapEntry(normalizedKey, converterFromJson(value)); + }) ?? + {}, + ); } @override T? resolve(Set states) { - //debugPrint("WidgetStateFromJSON states: $states, _states: $_states"); - // find specific state - for (var state in states) { - if (_states.containsKey(state.name)) { - return _states[state.name]!; + // Resolve using user-defined order in _states + for (var stateName in _states.keys) { + if (stateName == "default") continue; // Skip "default"; handled last + if (states.any((state) => state.name == stateName)) { + return _states[stateName]; } } - // catch-all value - if (_states.containsKey("")) { - return _states[""]; - } - - return _defaultValue; + // Default state + return _states["default"] ?? _defaultValue; } } diff --git a/sdk/python/packages/flet/src/flet/core/popup_menu_button.py b/sdk/python/packages/flet/src/flet/core/popup_menu_button.py index 57a2e157e..9c7049e0a 100644 --- a/sdk/python/packages/flet/src/flet/core/popup_menu_button.py +++ b/sdk/python/packages/flet/src/flet/core/popup_menu_button.py @@ -285,7 +285,6 @@ def __init__( self.items = items self.icon = icon self.on_cancel = on_cancel - self.on_cancelled = on_cancelled self.on_open = on_open self.shape = shape self.padding = padding diff --git a/sdk/python/packages/flet/src/flet/core/types.py b/sdk/python/packages/flet/src/flet/core/types.py index 814476c29..ba1b0b9bf 100644 --- a/sdk/python/packages/flet/src/flet/core/types.py +++ b/sdk/python/packages/flet/src/flet/core/types.py @@ -136,7 +136,7 @@ class ControlState(Enum): SCROLLED_UNDER = "scrolledUnder" DISABLED = "disabled" ERROR = "error" - DEFAULT = "" + DEFAULT = "default" class MainAxisAlignment(Enum): diff --git a/sdk/python/packages/flet/tests/test_datatable.py b/sdk/python/packages/flet/tests/test_datatable.py index fd15969d6..1b0864576 100644 --- a/sdk/python/packages/flet/tests/test_datatable.py +++ b/sdk/python/packages/flet/tests/test_datatable.py @@ -1,6 +1,7 @@ -import flet as ft from flet.core.protocol import Command +import flet as ft + def test_datatable_instance_no_attrs_set(): r = ft.DataTable(columns=[ft.DataColumn(label=ft.Text("Header"))]) @@ -38,7 +39,7 @@ def test_datarow_color_literal_material_state_as_string(): indent=0, name=None, values=["datarow"], - attrs={"color": '{"":"yellow"}'}, + attrs={"color": '{"default":"yellow"}'}, commands=[], ), Command(indent=2, name=None, values=["datacell"], attrs={}, commands=[]), @@ -51,7 +52,11 @@ def test_datarow_color_literal_material_state_as_string(): def test_datarow_color_multiple_material_states_as_strings(): r = ft.DataRow( cells=[ft.DataCell(content=ft.Text("Cell"))], - color={"selected": "red", "hovered": "blue", "": "yellow"}, + color={ + ft.ControlState.SELECTED: "red", + ft.ControlState.HOVERED: "blue", + ft.ControlState.DEFAULT: "yellow", + }, ) assert isinstance(r, ft.Control) assert r._build_add_commands() == [ @@ -59,7 +64,7 @@ def test_datarow_color_multiple_material_states_as_strings(): indent=0, name=None, values=["datarow"], - attrs={"color": '{"selected":"red","hovered":"blue","":"yellow"}'}, + attrs={"color": '{"selected":"red","hovered":"blue","default":"yellow"}'}, commands=[], ), Command(indent=2, name=None, values=["datacell"], attrs={}, commands=[]), @@ -84,7 +89,7 @@ def test_datarow_color_multiple_material_states(): indent=0, name=None, values=["datarow"], - attrs={"color": '{"selected":"red","hovered":"blue","":"yellow"}'}, + attrs={"color": '{"selected":"red","hovered":"blue","default":"yellow"}'}, commands=[], ), Command(indent=2, name=None, values=["datacell"], attrs={}, commands=[]), From 782133d4b893211fe2f9764104d123b0ee9c6ec6 Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Thu, 12 Dec 2024 23:35:53 +0100 Subject: [PATCH 08/12] fix wrong attribute name (#4557) --- packages/flet/lib/src/controls/dismissible.dart | 2 +- sdk/python/packages/flet/src/flet/core/dismissible.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/flet/lib/src/controls/dismissible.dart b/packages/flet/lib/src/controls/dismissible.dart index ea156cf85..582e97e30 100644 --- a/packages/flet/lib/src/controls/dismissible.dart +++ b/packages/flet/lib/src/controls/dismissible.dart @@ -56,7 +56,7 @@ class _DismissibleControlState extends State { parseDismissThresholds(widget.control, "dismissThresholds"); DismissDirection direction = parseDismissDirection( - widget.control.attrString("direction"), DismissDirection.horizontal)!; + widget.control.attrString("dismissDirection"), DismissDirection.horizontal)!; widget.backend.subscribeMethods(widget.control.id, (methodName, args) async { diff --git a/sdk/python/packages/flet/src/flet/core/dismissible.py b/sdk/python/packages/flet/src/flet/core/dismissible.py index 2fb8b1e9c..53f64ac2d 100644 --- a/sdk/python/packages/flet/src/flet/core/dismissible.py +++ b/sdk/python/packages/flet/src/flet/core/dismissible.py @@ -19,7 +19,6 @@ RotateValue, ScaleValue, ) -from flet.utils import deprecated class Dismissible(ConstrainedControl, AdaptiveControl): From 8d13aaea92ac73bc7236dace0d55fde9ca79d5c8 Mon Sep 17 00:00:00 2001 From: syle Date: Mon, 16 Dec 2024 09:46:50 -0600 Subject: [PATCH 09/12] Fix race condition on duration, add buffer --- packages/flet_video/lib/src/video.dart | 70 ++++++++++++------- .../packages/flet/src/flet/core/video.py | 1 + 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index b84f487cb..d39390c35 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -7,6 +7,7 @@ import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:audio_service/audio_service.dart'; import 'package:rxdart/rxdart.dart'; +import 'dart:async'; import 'utils/video.dart'; @@ -29,6 +30,8 @@ class VideoControl extends StatefulWidget { class _VideoControlState extends State with FletStoreMixin { int _lastProcessedIndex = -1; Duration _lastEmittedPosition = Duration.zero; + bool _trackChange = false; + late final playerConfig = PlayerConfiguration( title: widget.control.attrString("title", "Flet Video")!, muted: widget.control.attrBool("muted", false)!, @@ -98,12 +101,13 @@ class _VideoControlState extends State with FletStoreMixin { .triggerControlEvent(widget.control.id, "track_changed", message ?? ""); } - void _onPositionChanged(Duration position, Duration duration, int percent) { + void _onPositionChanged(Duration position, Duration duration, Duration buffer, int percent) { // commenting out, may be too verbose to display every 1 second - // debugPrint("New Position is ${position} and duration is ${duration} and percent is ${percent}"); + // debugPrint("New Position is ${position} seconds, duration is ${duration} seconds, buffer is ${buffer} seconds, and percent done ${percent}%"); final data = { "position": position.inSeconds, // Send position in seconds "duration": duration.inSeconds, // Send duration in seconds + "buffer": buffer.inSeconds, // Send buffer in seconds "percent": percent, }; widget.backend.triggerControlEvent(widget.control.id, "positionChanged", jsonEncode(data)); @@ -262,11 +266,11 @@ class _VideoControlState extends State with FletStoreMixin { break; case "next": debugPrint("Video.next($hashCode)"); - await player.next(); + await _audioHandler.skipToNext(); break; case "previous": debugPrint("Video.previous($hashCode)"); - await player.previous(); + await _audioHandler.skipToPrevious(); break; case "jump_to": debugPrint("Video.jump($hashCode)"); @@ -317,17 +321,30 @@ class _VideoControlState extends State with FletStoreMixin { _onCompleted(event.toString()); } }); - // Send percentage position change between 0-100 to use with flet slider. - // as well as throttling to 1 second to not overload flet socket + // Send position, duration, buffer and percent to Flet. + // Former 3 are in seconds, percent is percentage 0-100% of track completed + // Throttling defaults to 100ms(1 second) unless overridden, as well as no updates + // till seconds change to not overload flet socket. (buffer will return "None" on web) player.stream.position.throttleTime(Duration(milliseconds: throttle)).listen((position) { - if (onPositionChanged && position.inSeconds != _lastEmittedPosition.inSeconds) { - try { - final duration = player.state.duration; - final int percent = (position.inMilliseconds / duration.inMilliseconds * 100).toInt(); - _lastEmittedPosition = position; - _onPositionChanged(position, duration, percent); - } catch (e) { - debugPrint("Error in OnPositionChanged: $e"); + if (position.inSeconds != 0) { // stop race conditions on duration + if(_trackChange) { // ensure we send track changes to flet before new position updates + final int index = player.state.playlist.index; + if (onTrackChanged) { + _onTrackChanged(index.toString()); + } + _audioHandler.customAction('update_notification', {'index': index}); + _trackChange = false; + } + if (onPositionChanged && position.inSeconds != _lastEmittedPosition.inSeconds) { + try { + final Duration duration = player.state.duration; + final Duration buffer = player.state.buffer; + final int percent = (position.inMilliseconds / duration.inMilliseconds * 100).toInt(); + _lastEmittedPosition = position; + _onPositionChanged(position, duration, buffer, percent); + } catch (e) { + debugPrint("Error in OnPositionChanged: $e"); + } } } }); @@ -335,12 +352,11 @@ class _VideoControlState extends State with FletStoreMixin { player.stream.playlist.listen((event) { if (event.index != _lastProcessedIndex) { // prevent duplicates _lastProcessedIndex = event.index; - if (onTrackChanged) { - _onTrackChanged(event.index.toString()); - } - // We want this outside of onTrackChanged as we need to update - // notification bar regardless if they subscribed to handler or not. - _audioHandler.customAction('update_notification', {'index': event.index}); + // There is a race condition here, just because the track changed, does not mean duration + // has been updated yet, we will defeat it by letting player.stream.position + // check position.inSeconds != 0 before allowing update to notification, if track + // has been playing for 1 second we can be certain duration is then updated + _trackChange = true; } }); @@ -366,12 +382,11 @@ class MyAudioHandler extends BaseAudioHandler { ); @override - Future customAction(String name, - [Map? extras]) async { + Future customAction(String name, [Map? extras]) async { if (name == 'update_notification') { final index = extras?['index'] as int?; if (index != null) { - updatePlaybackState(index); + await updatePlaybackState(index); } } else { debugPrint("Unknown custom action: $name"); @@ -445,13 +460,14 @@ class MyAudioHandler extends BaseAudioHandler { } } - void updatePlaybackState(int index) { + Future updatePlaybackState(int index) async { final currentMedia = player.state.playlist.medias[index]; final extras = currentMedia.extras; // Url Decode and extract filename // I am going to assume structure of https://blah.com/Artist - SongName.mp3 // Let's attempt to split their filename into title/artist if they did not // include extras { title and artist } as full http url would be ugly + final filename = Uri.decodeFull(player.state.playlist.medias[index].uri.split('/').last); final parts = filename.split('.'); final filenameWithoutExtension = parts.sublist(0, parts.length - 1).join('.'); @@ -464,7 +480,7 @@ class MyAudioHandler extends BaseAudioHandler { final displayTitle = extras?['displayTitle'] as String?; final displaySubtitle = extras?['displaySubtitle'] as String?; final displayDescription = extras?['displayDescription'] as String?; - + mediaItem.add(MediaItem( id: index.toString(), title: title, @@ -477,7 +493,7 @@ class MyAudioHandler extends BaseAudioHandler { displayDescription: displayDescription != null ? displayDescription : null, duration: player.state.duration, )); - playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: Duration.zero)); - debugPrint("Now playing: ${player.state.playlist.medias[index].uri} with index ${player.state.playlist.index}"); + playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: player.state.position)); + // debugPrint("Now playing: ${Uri.decodeFull(player.state.playlist.medias[index].uri)} with index ${player.state.playlist.index}"); } } \ No newline at end of file diff --git a/sdk/python/packages/flet/src/flet/core/video.py b/sdk/python/packages/flet/src/flet/core/video.py index 1d388eb9c..e3dde735c 100644 --- a/sdk/python/packages/flet/src/flet/core/video.py +++ b/sdk/python/packages/flet/src/flet/core/video.py @@ -67,6 +67,7 @@ def __init__(self, e: ControlEvent): d = json.loads(e.data) self.position: int = d.get("position") self.duration: int = d.get("duration") + self.buffer: int = d.get("buffer") self.percent: int = d.get("percent") From 7dcf358be44a409e434551fd4fe25a79ed866d3b Mon Sep 17 00:00:00 2001 From: syle Date: Mon, 16 Dec 2024 09:51:31 -0600 Subject: [PATCH 10/12] Remove import not needed anymore --- packages/flet_video/lib/src/video.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index d39390c35..faa81588f 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -7,7 +7,6 @@ import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:audio_service/audio_service.dart'; import 'package:rxdart/rxdart.dart'; -import 'dart:async'; import 'utils/video.dart'; From b36fd829486a53129a4d7a254e4f1c6ef40a87ad Mon Sep 17 00:00:00 2001 From: syle Date: Mon, 16 Dec 2024 22:18:40 -0600 Subject: [PATCH 11/12] Faster check for updated duration --- packages/flet_video/lib/src/video.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index faa81588f..9abfbbc14 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -325,7 +325,7 @@ class _VideoControlState extends State with FletStoreMixin { // Throttling defaults to 100ms(1 second) unless overridden, as well as no updates // till seconds change to not overload flet socket. (buffer will return "None" on web) player.stream.position.throttleTime(Duration(milliseconds: throttle)).listen((position) { - if (position.inSeconds != 0) { // stop race conditions on duration + if (position != Duration.zero) { // stop race conditions on duration if(_trackChange) { // ensure we send track changes to flet before new position updates final int index = player.state.playlist.index; if (onTrackChanged) { @@ -353,8 +353,8 @@ class _VideoControlState extends State with FletStoreMixin { _lastProcessedIndex = event.index; // There is a race condition here, just because the track changed, does not mean duration // has been updated yet, we will defeat it by letting player.stream.position - // check position.inSeconds != 0 before allowing update to notification, if track - // has been playing for 1 second we can be certain duration is then updated + // check position != Duration.zero before allowing update to notification, if track + // has been playing for even a microsecond we can be certain duration is then updated _trackChange = true; } }); From a29acf7b75d4732781b5f227266d48fbd49b41b4 Mon Sep 17 00:00:00 2001 From: syle Date: Tue, 17 Dec 2024 00:37:09 -0600 Subject: [PATCH 12/12] Do not delay sending flet track changes --- packages/flet_video/lib/src/video.dart | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/flet_video/lib/src/video.dart b/packages/flet_video/lib/src/video.dart index 9abfbbc14..7fafdab39 100644 --- a/packages/flet_video/lib/src/video.dart +++ b/packages/flet_video/lib/src/video.dart @@ -322,16 +322,13 @@ class _VideoControlState extends State with FletStoreMixin { }); // Send position, duration, buffer and percent to Flet. // Former 3 are in seconds, percent is percentage 0-100% of track completed - // Throttling defaults to 100ms(1 second) unless overridden, as well as no updates - // till seconds change to not overload flet socket. (buffer will return "None" on web) + // Throttling defaults to 1000ms(1 second) unless overridden, as well as no updates + // till seconds change to not overload flet socket. player.stream.position.throttleTime(Duration(milliseconds: throttle)).listen((position) { - if (position != Duration.zero) { // stop race conditions on duration - if(_trackChange) { // ensure we send track changes to flet before new position updates - final int index = player.state.playlist.index; - if (onTrackChanged) { - _onTrackChanged(index.toString()); - } - _audioHandler.customAction('update_notification', {'index': index}); + if (position != Duration.zero) { // this ensures duration is updated as track changes + if(_trackChange) { + // Duration finally changed, update notification for 2nd time with correct duration + _audioHandler.customAction('update_notification', {'index': player.state.playlist.index}); _trackChange = false; } if (onPositionChanged && position.inSeconds != _lastEmittedPosition.inSeconds) { @@ -351,10 +348,15 @@ class _VideoControlState extends State with FletStoreMixin { player.stream.playlist.listen((event) { if (event.index != _lastProcessedIndex) { // prevent duplicates _lastProcessedIndex = event.index; + if (onTrackChanged) { + _onTrackChanged(event.index.toString()); + } + _audioHandler.customAction('update_notification', {'index': event.index}); // There is a race condition here, just because the track changed, does not mean duration - // has been updated yet, we will defeat it by letting player.stream.position - // check position != Duration.zero before allowing update to notification, if track - // has been playing for even a microsecond we can be certain duration is then updated + // has been updated yet, but we don't want to delay sending Flet track change either + // so it can update it's UI quickly. A stream on duration won't work because if 2 songs + // have same duration that won't trigger. Best way to handle this then is update notification + // one more time when duration does change, by letting position stream handle it. _trackChange = true; } }); @@ -467,7 +469,7 @@ class MyAudioHandler extends BaseAudioHandler { // Let's attempt to split their filename into title/artist if they did not // include extras { title and artist } as full http url would be ugly - final filename = Uri.decodeFull(player.state.playlist.medias[index].uri.split('/').last); + final filename = Uri.decodeFull(currentMedia.uri.split('/').last); final parts = filename.split('.'); final filenameWithoutExtension = parts.sublist(0, parts.length - 1).join('.'); final artistAndTitle = filenameWithoutExtension.split('-'); @@ -493,6 +495,5 @@ class MyAudioHandler extends BaseAudioHandler { duration: player.state.duration, )); playbackState.add(_basePlaybackState.copyWith(playing: player.state.playing, updatePosition: player.state.position)); - // debugPrint("Now playing: ${Uri.decodeFull(player.state.playlist.medias[index].uri)} with index ${player.state.playlist.index}"); } } \ No newline at end of file