Flutter audio slider with assets_audio_player - flutter

I'm trying to create a slider to watch the audio progress and seek some parts of the audio. In order to play local audio files I'm using the assets_audio_player package. This is my code:
import 'package:assets_audio_player/assets_audio_player.dart';
...
...
final AssetsAudioPlayer _assetsAudioPlayer = AssetsAudioPlayer();
...
...
StreamBuilder<Duration>(
stream: _assetsAudioPlayer.currentPosition,
builder: (BuildContext context, AsyncSnapshot <Duration> snapshot) {
final Duration _currentDuration = snapshot.data;
final int _milliseconds = _currentDuration.inMilliseconds;
final int _songDurationInMilliseconds = snapshot.data.inMilliseconds;
return Slider(
min: 0,
max: _songDurationInMilliseconds.toDouble(),
value: _songDurationInMilliseconds > _milliseconds
? _milliseconds.toDouble()
: _songDurationInMilliseconds.toDouble(),
onChanged: (double value) {
_assetsAudioPlayer.seek(Duration(milliseconds: (value / 1000.0).toInt()));
},
activeColor: Colors.blue,
inactiveColor: Colors.grey,
);
},
),
However, the behaviour of the slider is far away from the expected. I can't seek and it doesn't move. How to solve this?

You can directly use https://github.com/samupra/local_flutter_audio_player
It has all features you need
code snippet
Widget slider() {
return Slider(
value: _position.inSeconds.toDouble(),
min: 0.0,
max: _duration.inSeconds.toDouble(),
onChanged: (double value) {
setState(() {
seekToSecond(value.toInt());
value = value;
});});
}
Widget localAsset() {
return _tab([
Text('Play Local Asset \'audio.mp3\':'),
_btn('Play', () => audioCache.play('audio.mp3')),
_btn('Pause',() => advancedPlayer.pause()),
_btn('Stop', () => advancedPlayer.stop()),
slider()
]);
}
working demo
full code
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
typedef void OnError(Exception exception);
void main() {
runApp(new MaterialApp(home: new ExampleApp()));
}
class ExampleApp extends StatefulWidget {
#override
_ExampleAppState createState() => new _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
Duration _duration = new Duration();
Duration _position = new Duration();
AudioPlayer advancedPlayer;
AudioCache audioCache;
#override
void initState(){
super.initState();
initPlayer();
}
void initPlayer(){
advancedPlayer = new AudioPlayer();
audioCache = new AudioCache(fixedPlayer: advancedPlayer);
advancedPlayer.durationHandler = (d) => setState(() {
_duration = d;
});
advancedPlayer.positionHandler = (p) => setState(() {
_position = p;
});
}
String localFilePath;
Widget _tab(List<Widget> children) {
return Center(
child: Container(
padding: EdgeInsets.all(16.0),
child: Column(
children: children
.map((w) => Container(child: w, padding: EdgeInsets.all(6.0)))
.toList(),
),
),
);
}
Widget _btn(String txt, VoidCallback onPressed) {
return ButtonTheme(
minWidth: 48.0,
child: RaisedButton(child: Text(txt), onPressed: onPressed));
}
Widget slider() {
return Slider(
value: _position.inSeconds.toDouble(),
min: 0.0,
max: _duration.inSeconds.toDouble(),
onChanged: (double value) {
setState(() {
seekToSecond(value.toInt());
value = value;
});});
}
Widget localAsset() {
return _tab([
Text('Play Local Asset \'audio.mp3\':'),
_btn('Play', () => audioCache.play('audio.mp3')),
_btn('Pause',() => advancedPlayer.pause()),
_btn('Stop', () => advancedPlayer.stop()),
slider()
]);
}
void seekToSecond(int second){
Duration newDuration = Duration(seconds: second);
advancedPlayer.seek(newDuration);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 1,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(text: 'Local Asset'),
],
),
title: Text('audioplayers Example'),
),
body: TabBarView(
children: [localAsset()],
),
),
);
}
}

I abandoned this project over a year ago, but this is the code I used to make it work. I don't remember too much details about it, but hope it helps:
_selectedIndex == index && (songDuration > 10000.0)// && playing == true
? Row(
children: <Widget>[
Flexible(
flex: 2,
child: Container(
width: double.infinity,
),
),
Flexible(
flex: 14,
child: StreamBuilder<Duration>(
stream: _assetsAudioPlayer.currentPosition,
builder: (BuildContext context, AsyncSnapshot<Duration> snapshot) {
if (!snapshot.hasData) {
return Slider(
value: 0.0,
onChanged: (double value) => null,
activeColor: Colors.transparent,
inactiveColor: Colors.transparent,
);
}
final PlayingAudio playing2 = _assetsAudioPlayer.current.value;
final Duration position = snapshot.data;
// bool _finish = _assetsAudioPlayer.finished.value;
// print(playing2);
return Slider(
min: 0.0,
max: playing2.duration.inMilliseconds.toDouble() - 0.01,
value: position.inMilliseconds.toDouble() == playing2.duration.inMilliseconds.toDouble()
? 0.0
: position.inMilliseconds.toDouble(),
onChanged: (double value) {
_assetsAudioPlayer.seek(Duration(milliseconds: value.toInt()));
value = value;
},
onChangeEnd: (double value) {
},
activeColor: Colors.blue,
inactiveColor: Color(0xFFCEE3EE),
);
},
),
),
],
) : Container(),

Related

Unhandled Exception: setState() called after dispose() - due to modal dissmissed

I have a modalBottomSheet. On it, I am displaying several widgets, especially an audioPlayer.
I have find out that when I press the play button, the audio file is playing, so far so good, but if I tap outside the modalBottomSheet, the modal is dismissed. I am OK to get the modal dismissed. But my problem is that when it is dismissed, the player which is running, is generating an exception.
Unhandled Exception: setState() called after dispose()
I do not want to make the Modal not dissmisible.
Please, can you advise? Many thanks.
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers/audioplayers_api.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:gtd_official_sharped_focused/Reusable%20Widget/Player_Audio/widgets/play_pause_button.dart';
class AudioPlayerWidget extends StatefulWidget {
final String url;
final bool isAsset;
final Duration currentTime;
final Duration totalTime;
final ValueChanged<Duration> onSeekBarMoved;
const AudioPlayerWidget({
Key key,
this.url,
this.isAsset = false,
this.currentTime,
this.totalTime,
this.onSeekBarMoved,
}) : super(key: key);
#override
_AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();
}
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
AudioPlayer _audioPlayer;
AudioCache _audioCache;
//variables for slider
Duration _duration = new Duration();
Duration _position = new Duration();
PlayerState _playerState = PlayerState.STOPPED;
bool get _isPlaying => _playerState == PlayerState.PLAYING;
bool get _isLocal => !widget.url.contains('https');
#override
void initState() {
_audioPlayer = AudioPlayer(mode: PlayerMode.MEDIA_PLAYER);
_audioCache = AudioCache(fixedPlayer: _audioPlayer);
_audioPlayer.onDurationChanged.listen((d) {setState(() {
_duration = d;
});});
_audioPlayer.onAudioPositionChanged.listen((p) {setState((){
_position = p;
});});
_audioPlayer.onPlayerCompletion.listen((event) {
setState(() {
_position = Duration(seconds: 0);
_playerState = PlayerState.STOPPED;
});
});
_audioPlayer.onPlayerError.listen((msg) {
print('audioPlayer error : $msg');
setState(() {
_playerState = PlayerState.STOPPED;
});
});
super.initState();
}
#override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left:18.0),
child: Text(_position.toString().split('.')[0],style:TextStyle(fontSize: 16)),
),
Padding(
padding: const EdgeInsets.only(right:18.0),
child: Text(_duration.toString().split('.')[0],style:TextStyle(fontSize: 16)),
),
],),
_buildSliderBar(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
buttonBackWard10Seconds(),
PlayPauseButton(
isPlaying: _isPlaying,
onPlay: () => _playPause()
),
buttonForward10Seconds(),
//Do not delete iconButton below => for reference
/* IconButton(
onPressed: () => _stop(),
icon: Icon(
Icons.stop,
size: 40,
color: Colors.red,
),
),*/
],
),
],
);
}
//########################################################
_playPause() async {
if (_playerState == PlayerState.PLAYING) {
final playerResult = await _audioPlayer.pause();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PAUSED;
});
}
} else if (_playerState == PlayerState.PAUSED) {
final playerResult = await _audioPlayer.resume();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PLAYING;
});
}
} else {
if (widget.isAsset) {
_audioPlayer = await _audioCache.play(widget.url);
setState(() {
_playerState = PlayerState.PLAYING;
});
} else {
final playerResult = await _audioPlayer.play(widget.url, isLocal: _isLocal);
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PLAYING;
});
}
}
}
}
void changeToSecond(int second){
Duration newDuration = Duration(seconds:second);
_audioPlayer.seek(newDuration);
}
_stop() async {
final playerResult = await _audioPlayer.stop();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.STOPPED;
});
}
}
//###############################################################
Slider _buildSliderBar(BuildContext context) {
return Slider(
value: _position.inSeconds.toDouble(),
min: 0.0,
max: _duration.inSeconds.toDouble(), //_sliderValue,
activeColor: Colors.red,
inactiveColor: Colors.grey,
onChanged: (double value) {
setState(() {
changeToSecond(value.toInt());
value=value;
});
},
);
}
Widget buttonBackWard10Seconds(){
return IconButton( icon: Icon(CupertinoIcons.gobackward_10),
iconSize: 40,
color: Colors.black,
onPressed: (){
_position = _position - Duration(seconds:10);
if (_position < Duration(seconds:0)) {
_audioPlayer.seek(Duration(seconds: 0));
}
else {
_audioPlayer.seek(_position);
}});
}
Widget buttonForward10Seconds(){
return IconButton( icon:Icon( CupertinoIcons.goforward_10),
iconSize: 40,
color: Colors.black,
onPressed: (){
_position = _position + Duration(seconds:10);
if (_duration >_position) {
_audioPlayer.seek(_position);
}
else if (_duration <_position) {
_audioPlayer.seek(_duration);
}
}
);
}
}
import 'package:flutter/material.dart';
import 'package:gtd_official_sharped_focused/Reusable%20Widget/Player_Audio/widgets/audio_player_widget.dart';
import 'package:gtd_official_sharped_focused/Services/extract_file_Name_url/extract_file_name_url.dart';
Widget modalBottomPlayAudio (context,String urlToPlay){
showModalBottomSheet(
context: context,
//background color for modal bottom screen
backgroundColor: Colors.white,
//elevates modal bottom screen
elevation: 10,
// gives rounded corner to modal bottom screen
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
builder: (BuildContext context) {
// UDE : SizedBox instead of Container for whitespaces
return SizedBox(
height: 350,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
/*Padding(
padding: const EdgeInsets.all(28.0),
),*/
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left:18.0),
child: Text(getFileNameFromURL(urlToPlay),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
),
],
),
SizedBox(height: 60,),
AudioPlayerWidget(url:urlToPlay),
],
),
),
);
},
);
}
you could change
setState(()=>{
...
})
to
if(mounted)(
setState(()=>{
...
})
)
Which ensures setState is called only when the widget is mounted on the screen.

PageView is consuming all gestures with embedded webview in flutter

I have an embedded web view that is nested alongside a pageview builder which controls my three js animations by sending javascript to the embedded web view.
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_museum/app/data/bloc/artifacts.dart';
import 'package:flutter_museum/app/domain/models/artifact_model.dart';
import 'package:flutter_museum/app/ui/discover/widgets/animatedCardTutorial.dart';
import 'package:flutter_museum/app/ui/discover/widgets/focusPointList.dart';
import 'package:flutter_museum/app/ui/discover/widgets/questionMarkButton.dart';
import 'package:flutter_museum/app/ui/discover/widgets/threeJSViewer.dart';
import 'package:flutter_museum/app/ui/widgets/assetOrCachedNetworkImage.dart';
import 'package:flutter_museum/app/ui/widgets/circularBackButton.dart';
import 'package:flutter_museum/app/ui/widgets/simpleVideo.dart';
import 'package:flutter_museum/app/ui/widgets/videoPlayerControllers.dart';
double deviceHeight = 0.0;
double deviceWidth = 0.0;
class ArtifactView extends StatefulWidget {
ArtifactView({
Key? key,
}) : super(key: key);
#override
State<ArtifactView> createState() => _ArtifactViewState();
}
class _ArtifactViewState extends State<ArtifactView> {
bool _fullscreen = false;
AbstractVideoPlayerController? videoPlayerController;
Timer? _restoreUiTimer;
int? startUpCount;
bool shouldShowTutorial = false;
void startFullScreen() {
if (!_fullscreen) {
setState(() {
_fullscreen = true;
_restoreUiTimer?.cancel();
_restoreUiTimer = Timer(Duration(milliseconds: 1500), endFullScreen);
});
}
}
void endFullScreen() {
setState(() {
_fullscreen = false;
});
}
void hideTutorial() {
setState(() {
shouldShowTutorial = false;
});
}
#override
void initState() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
super.initState();
}
#override
void dispose() {
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
videoPlayerController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// hide status bar when displaying fullscreen artifact. (otherwise show status bar)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: _fullscreen ? [] : SystemUiOverlay.values);
return BlocConsumer<ArtifactsBloc, ArtifactsState>(
listener: (context, state) {},
builder: (context, state) {
Artifact artifact = state.hasArtifact ? state.currentArtifact : Artifact.empty;
//List<FocusPoint> focusPoints = state.hasArtifact ? state.focusPoints : [];
//String modelFilePath = state.modelPath;
//bool modelIsAsset = isAssetUrl(modelFilePath);
if (videoPlayerController == null && artifact.uuid == 'ironman_artifact') {
// EasterEgg // TODO - have Kodi add the polka.mp3 file to our CDN and reference that below instead of userbob.com
videoPlayerController = createVideoPlayerController('https://userbob.com/motb/polka.mp3', useChewie: true);
videoPlayerController?.initialize();
videoPlayerController?.play();
}
// FIXME - trying to always display background sized to device's dimensions so that the background
// doesn't shift when switching to full-screen mode. The code below will prevent the background from
// shifting after the first time we've gone into full-screen mode. However, the very first time
// their will be a slight shift.
//!FIXME - We should look into the overlfow widdget to let the scaffold know we intend to let this widget be larger than the screen.
var windowSize = MediaQuery.of(context).size;
if (windowSize.height > deviceHeight) deviceHeight = windowSize.height;
if (windowSize.width > deviceWidth) deviceWidth = windowSize.width;
return Center(
child: Container(
height: deviceHeight,
width: deviceWidth,
child: Stack(
children: [
if (videoPlayerController != null)
SimpleVideo(
placeholderImage: 'asset://images/discover/washingtonrevelations.webp',
videoPlayerController: videoPlayerController!,
),
if (artifact.backgroundUrl.isNotEmpty)
Container(
height: deviceHeight,
width: deviceWidth,
child: AssetOrCachedNetworkImage(imageUrl: artifact.backgroundUrl, fit: BoxFit.cover),
),
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
radius: 0.9,
colors: [
Colors.transparent,
Colors.transparent,
Colors.black38,
],
),
),
height: deviceHeight,
width: deviceWidth,
),
Listener(
onPointerDown: (PointerDownEvent e) {
startFullScreen();
},
child: GestureDetector(
onPanEnd: ((details) {
print('ended');
}),
child: ThreeJSViewer(
modelFilePath: state.modelPath,
initScale: artifact.cameraZoom.toInt(),
autoRotateSpeed: 2,
state: state,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: EdgeInsets.only(top: 35),
child: CircularBackButton(
outterPadding: EdgeInsets.fromLTRB(10, 10, 0, 0),
onTap: () {
AutoRouter.of(context).pop();
},
),
),
),
AnimatedPositioned(
bottom: 60 - (_fullscreen ? 250 : 0.0),
duration: Duration(milliseconds: 300),
child: GestureDetector(
child: FocusPointList(
state: state,
),
),
),
],
),
),
);
},
);
}
}
finally here is my WebView plus widget
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_museum/app/data/bloc/artifact/artifacts_state.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:webview_flutter_plus/webview_flutter_plus.dart';
// ignore: must_be_immutable
class ThreeJSViewer extends StatefulWidget {
final String? modelFilePath;
final String? modelURLPath;
final double? autoRotateSpeed;
final int initScale;
final ArtifactsState state;
final bool? debug;
final void Function(PointerMoveEvent)? onPointerMove;
ThreeJSViewer({
Key? key,
this.modelFilePath,
this.onPointerMove,
this.modelURLPath,
required this.initScale,
this.autoRotateSpeed,
required this.state,
this.debug,
}) : super(key: key);
#override
State<ThreeJSViewer> createState() => _ThreeJSViewerState();
}
class _ThreeJSViewerState extends State<ThreeJSViewer> {
int progress = 0;
WebViewPlusController? controller;
bool isRendered = false;
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: (context, snapshot) {
if (snapshot.data == true) {
return Stack(
children: [
if (progress < 100 && isRendered == true)
Align(
alignment: Alignment.center,
child: Container(
height: 100,
width: 100,
child: CircularProgressIndicator(
value: progress.toDouble(),
),
),
),
WebViewPlus(
initialCookies: [],
gestureNavigationEnabled: true,
zoomEnabled: false,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
new Factory<OneSequenceGestureRecognizer>(
() {
print('GestureRecognizer created');
return new EagerGestureRecognizer();
},
),
].toSet(),
backgroundColor: Colors.transparent,
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: ({
JavascriptChannel(
name: 'JavascriptChannel',
onMessageReceived: (message) {
if (message == "200") {
setState(() {
isRendered = true;
});
}
log(message.message);
},
)
}),
onProgress: (_progress) {
setState(() {
progress = _progress;
});
},
onWebViewCreated: (_controller) {
controller = _controller;
_controller.loadUrl("assets/renderer/index.html");
},
onPageFinished: (value) {
Future.delayed(Duration(milliseconds: 100), () {
this.widget.state.controller = controller?.webViewController;
this.widget.state.controller!.runJavascript(
'init("${this.widget.modelFilePath ?? this.widget.modelURLPath}", ${this.widget.initScale},${this.widget.initScale},${this.widget.initScale}, ${this.widget.autoRotateSpeed ?? 2}, ${this.widget.debug ?? false})',
);
});
},
onPageStarted: (value) {
log(value);
},
onWebResourceError: (value) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading model. Please make sure you have internet connection.'),
),
);
log(value.description);
},
),
],
);
} else {
return Center(
child: Container(
child: DefaultTextStyle(
style: TextStyle(color: Colors.white, fontSize: 25),
child: Text(
"no internet connection",
),
),
),
);
}
},
future: InternetConnectionChecker().hasConnection,
);
}
}
My WebView is able to accept all gesture events when I initially rotate or zoom the three js model, but when I swipe a card on the pageview builder it consumes whatever gesture event for the duration of the widget lifetime.
finally here are the pageview cards
import 'package:flutter/material.dart';
import 'package:flutter_museum/app/data/bloc/artifact/artifacts_state.dart';
import 'package:flutter_museum/app/domain/models.dart';
import 'package:flutter_museum/app/ui/discover/views/focusPointView.dart';
import 'package:flutter_museum/app/ui/discover/widgets/focusPointCard.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_museum/app/data/bloc/artifacts.dart';
class FocusPointList extends StatefulWidget {
final ArtifactsState state;
const FocusPointList({Key? key, required this.state}) : super(key: key);
#override
State<FocusPointList> createState() => _FocusPointListState();
}
class _FocusPointListState extends State<FocusPointList> {
#override
Widget build(BuildContext context) {
int _centeredFocusPointIndex = 0;
ArtifactsState _state = this.widget.state;
List<FocusPoint> focusPoints = _state.hasArtifact ? _state.focusPoints : [];
List artifactFocusPointsList = focusPoints
.map(
(fp) => FocusPointCard(
publisher: fp.publisher ?? '',
subtitle: fp.subtitle ?? '',
title: fp.title,
imageUrl: fp.thumbnailImageUrl,
externalLink: fp.externalLink,
cardOnTap: () => _handleTapOnCard(context: context, focusPoint: fp),
),
)
.toList();
return Container(
height: 160,
width: MediaQuery.of(context).size.width,
child: PageView.builder(
itemCount: artifactFocusPointsList.length,
controller: PageController(viewportFraction: 0.75),
onPageChanged: (int index) {
setState(() {
_centeredFocusPointIndex = index;
FocusPoint afp = focusPoints[index];
bool isPointsEmpty = afp.x + afp.y + afp.z != 0;
if (index < focusPoints.length - 1 && isPointsEmpty) {
_centeredFocusPointIndex = index;
_state.controller?.runJavascript('tweenCamera(${afp.x}, ${afp.y}, ${afp.z}, 2000);');
}
});
},
itemBuilder: (_, index) {
return Transform.scale(
scale: index == _centeredFocusPointIndex ? 1 : 0.9,
child: artifactFocusPointsList[index],
);
},
),
);
}
void _handleTapOnCard({required BuildContext context, required FocusPoint focusPoint}) {
if (focusPoint.externalLink != null) {
launch(focusPoint.externalLink!);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FocusPointView(
title: focusPoint.title,
subtitle: focusPoint.subtitle ?? '',
publisher: focusPoint.publisher ?? '',
imageUrl: focusPoint.detailImageUrl ?? '',
description: focusPoint.description ?? '',
),
),
);
}
}
}

Convert Text to Speech with duration and progress bar

I have an issue in achieving text to speech conversion, am using flutter tts plugin for the text to audio converting, but i also need the duration of the audio for the progress bar and also for the timer but i could not able to get how i can achieve this,
this is what i need to achieve
i have tried this using flutterTts progress handler, but it only gives the offset values of start and end, am getting the text from API.
_flutterTts.setProgressHandler((String text, int startOffset, int endOffset, String word) {
setState(() {
_currentWord = word;
});
});
kindly help me to get the duration of the audio using flutterTts plugin,
any suggestion would be appreciated.
Given the example in the flutter_tts repo, you can simply add a progressHandler and LinearProgressIndicator.
Here is an example:
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
enum TtsState { playing, stopped, paused, continued }
class _MyAppState extends State<MyApp> {
late FlutterTts flutterTts;
dynamic languages;
String? language;
double volume = 0.5;
double pitch = 1.0;
double rate = 0.5;
bool isCurrentLanguageInstalled = false;
int end = 0;
String? _newVoiceText;
TtsState ttsState = TtsState.stopped;
get isPlaying => ttsState == TtsState.playing;
get isStopped => ttsState == TtsState.stopped;
get isPaused => ttsState == TtsState.paused;
get isContinued => ttsState == TtsState.continued;
bool get isIOS => !kIsWeb && Platform.isIOS;
bool get isAndroid => !kIsWeb && Platform.isAndroid;
bool get isWeb => kIsWeb;
#override
initState() {
super.initState();
initTts();
}
initTts() {
flutterTts = FlutterTts();
_getLanguages();
if (isAndroid) {
_getEngines();
}
flutterTts.setStartHandler(() {
setState(() {
print("Playing");
ttsState = TtsState.playing;
});
});
flutterTts.setCompletionHandler(() {
setState(() {
print("Complete");
ttsState = TtsState.stopped;
});
});
flutterTts.setCancelHandler(() {
setState(() {
print("Cancel");
ttsState = TtsState.stopped;
});
});
if (isWeb || isIOS) {
flutterTts.setPauseHandler(() {
setState(() {
print("Paused");
ttsState = TtsState.paused;
});
});
flutterTts.setContinueHandler(() {
setState(() {
print("Continued");
ttsState = TtsState.continued;
});
});
}
flutterTts.setErrorHandler((msg) {
setState(() {
print("error: $msg");
ttsState = TtsState.stopped;
});
});
flutterTts.setProgressHandler(
(String text, int startOffset, int endOffset, String word) {
setState(() {
end = endOffset;
});
});
}
Future _getLanguages() async {
languages = await flutterTts.getLanguages;
if (languages != null) setState(() => languages);
}
Future _getEngines() async {
var engines = await flutterTts.getEngines;
if (engines != null) {
for (dynamic engine in engines) {
print(engine);
}
}
}
Future _speak() async {
await flutterTts.setVolume(volume);
await flutterTts.setSpeechRate(rate);
await flutterTts.setPitch(pitch);
if (_newVoiceText != null) {
await flutterTts.awaitSpeakCompletion(true);
await flutterTts.speak(_newVoiceText!);
}
}
Future _stop() async {
var result = await flutterTts.stop();
if (result == 1) setState(() => ttsState = TtsState.stopped);
}
Future _pause() async {
var result = await flutterTts.pause();
if (result == 1) setState(() => ttsState = TtsState.paused);
}
#override
void dispose() {
super.dispose();
flutterTts.stop();
}
List<DropdownMenuItem<String>> getLanguageDropDownMenuItems() {
var items = <DropdownMenuItem<String>>[];
for (dynamic type in languages) {
items.add(DropdownMenuItem(value: type as String, child: Text(type)));
}
return items;
}
void changedLanguageDropDownItem(String? selectedType) {
setState(() {
language = selectedType;
flutterTts.setLanguage(language!);
if (isAndroid) {
flutterTts
.isLanguageInstalled(language!)
.then((value) => isCurrentLanguageInstalled = (value as bool));
}
});
}
void _onChange(String text) {
setState(() {
_newVoiceText = text;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter TTS'),
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(children: [
_inputSection(),
ttsState == TtsState.playing ? _progressBar(end) : Text(""),
_btnSection(),
languages != null ? _languageDropDownSection() : Text(""),
_buildSliders()
]))));
}
Widget _inputSection() => Container(
alignment: Alignment.topCenter,
padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0),
child: TextField(
onChanged: (String value) {
_onChange(value);
},
));
Widget _progressBar(int end) => Container(
alignment: Alignment.topCenter,
padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0),
child: LinearProgressIndicator(
backgroundColor: Colors.red,
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
value: end / _newVoiceText!.length,
));
Widget _btnSection() {
if (isAndroid) {
return Container(
padding: EdgeInsets.only(top: 50.0),
child:
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
_buildButtonColumn(Colors.green, Colors.greenAccent,
Icons.play_arrow, 'PLAY', _speak),
_buildButtonColumn(
Colors.red, Colors.redAccent, Icons.stop, 'STOP', _stop),
]));
} else {
return Container(
padding: EdgeInsets.only(top: 50.0),
child:
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
_buildButtonColumn(Colors.green, Colors.greenAccent,
Icons.play_arrow, 'PLAY', _speak),
_buildButtonColumn(
Colors.red, Colors.redAccent, Icons.stop, 'STOP', _stop),
_buildButtonColumn(
Colors.blue, Colors.blueAccent, Icons.pause, 'PAUSE', _pause),
]));
}
}
Widget _languageDropDownSection() => Container(
padding: EdgeInsets.only(top: 50.0),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
DropdownButton(
value: language,
items: getLanguageDropDownMenuItems(),
onChanged: changedLanguageDropDownItem,
),
Visibility(
visible: isAndroid,
child: Text("Is installed: $isCurrentLanguageInstalled"),
),
]));
Column _buildButtonColumn(Color color, Color splashColor, IconData icon,
String label, Function func) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(icon),
color: color,
splashColor: splashColor,
onPressed: () => func()),
Container(
margin: const EdgeInsets.only(top: 8.0),
child: Text(label,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w400,
color: color)))
]);
}
Widget _buildSliders() {
return Column(
children: [_volume(), _pitch(), _rate()],
);
}
Widget _volume() {
return Slider(
value: volume,
onChanged: (newVolume) {
setState(() => volume = newVolume);
},
min: 0.0,
max: 1.0,
divisions: 10,
label: "Volume: $volume");
}
Widget _pitch() {
return Slider(
value: pitch,
onChanged: (newPitch) {
setState(() => pitch = newPitch);
},
min: 0.5,
max: 2.0,
divisions: 15,
label: "Pitch: $pitch",
activeColor: Colors.red,
);
}
Widget _rate() {
return Slider(
value: rate,
onChanged: (newRate) {
setState(() => rate = newRate);
},
min: 0.0,
max: 1.0,
divisions: 10,
label: "Rate: $rate",
activeColor: Colors.green,
);
}
}

Flutter appear The method 'findRenderObject' was called on null

I have a need recently,I need to measure the distance between the child elements in the sliver and the top, but always prompt that findrendereobject is empty。
I can't even try widgetsbinding.instance.addpostframecallback
Console error:
════════ Exception caught by scheduler library
═════════════════════════════════════════════════════
The following
NoSuchMethodError was thrown during a scheduler callback: The method
'findRenderObject' was called on null. Receiver: null Tried calling:
findRenderObject()
Please Everybody look what happened,why can't measuring height~thank you very much!!!
My Code:
class GoodsDetailPage extends StatefulWidget {
final IndexVOS bean;
GoodsDetailPage(this.bean);
#override
_GoodsDetailPageState createState() => _GoodsDetailPageState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> with SingleTickerProviderStateMixin{
int productId;
int areaId;
String serviceTime;
ProductInfoBean productInfoBean;
String _selectName;
Norms norms;
double screenWidth;
bool _isShow = false;
TabController _tabController;
List<String> tabList;
ScrollController _scrollController = ScrollController();
var globalKeyOne = GlobalKey();
var globalKeyTwo = GlobalKey();
var globalKeyThree = GlobalKey();
var oneY = 0.0;
var twoY = 0.0;
var threeY = 0.0;
ProductModel model = ProductModel();
GoodsSpecModel _specModel = GoodsSpecModel();
#override
void initState() {
productId = widget.bean.productId;
areaId = widget.bean.areaId;
serviceTime = widget.bean.serviceTimeBegin;
tabList = [
"商品",
"评价",
"详情",
];
_tabController = TabController(
length: tabList.length,
vsync: this,
);
_scrollController.addListener(() {
var of = _scrollController.offset;
setState(() {
_isShow = of >= screenWidth-50;
});
if (of > threeY - oneY) {
_tabController.animateTo(2);
}else if (of > twoY - oneY) {
_tabController.animateTo(1);
} else {
_tabController.animateTo(0);
}
print("滚动了$of one=${twoY - oneY}=two=${threeY - oneY}");
});
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
//等待界面渲染完成
void _afterLayout(_){
oneY = getY(globalKeyOne.currentContext);
twoY = getY(globalKeyTwo.currentContext);
threeY = getY(globalKeyThree.currentContext);
}
static double getY(BuildContext buildContext) {
final RenderBox box = buildContext.findRenderObject();
//final size = box.size;
final topLeftPosition = box.localToGlobal(Offset.zero);
return topLeftPosition.dy;
}
Future _request() async{
model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime);
}
#override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: ProviderWidget<ProductModel>(
model: model,
onModelReady: (model) => model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime),
builder: (context, model, child) {
if (model.busy) {
return ViewStateBusyWidget();
}
if (model.error) {
return ViewStateErrorWidget(error: model.viewStateError, onPressed: _request);
}
return Column(
children: <Widget>[
Expanded(child: _body()),
_bottom()
],
);
}
),
)
);
}
Widget _body(){
var _normalBack = Image.asset(ImagePath.normalBack,height: dp40,width: dp40);
var _detailBack = Image.asset(ImagePath.detailBack,height: dp60,width: dp60);
var _normalShare = Image.asset(ImagePath.detailShareSmall,height: dp40,width: dp60);
var _detailShare = Image.asset(ImagePath.detailShare,height: dp60,width: dp140);
var _normalDot = Image.asset(ImagePath.threeDot,height: dp40,width: dp40);
var _detailDot = Image.asset(ImagePath.detailDot,height: dp60,width: dp60);
return CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
key: globalKeyOne,
title: _isShow?_topTabTitle():Container(),
centerTitle: true,
expandedHeight: screenWidth,
floating: false,
pinned: true,
snap: false,
elevation: 0.5,
leading: IconButton(
icon: _isShow?_normalBack:_detailBack,
onPressed: () => pop(),
),
actions: <Widget>[
GestureDetector(
child: _isShow?_normalShare:_detailShare,
onTap: (){
print("分享");
},
),
SizedBox(width: dp24),
GestureDetector(
child: _isShow?_normalDot:_detailDot,
onTap: _showPopup,
),
SizedBox(width: dp30),
],
flexibleSpace: FlexibleSpaceBar(
background: GoodsDetailBannerWidget(model),
),
),
SliverList(
delegate: SliverChildListDelegate([
_activityImage(),
GoodsDetailInfoWidget(model),
gap20,
GoodsDetailOtherWidget(model,serviceTime),
GoodsDetailCategoryWidget(click: _clickSpec,name: _selectName),
gap20,
Column(
key: globalKeyTwo,
children: <Widget>[
GoodsDetailCommentWidget(model),
gap20,
],
),
Container(
key: globalKeyThree,
child: GoodsDetailDescWidget(model),
)
]),
),
],
);
}
Widget _bottom(){
return GoodsDetailBottomWidget(
clickBuy: (){
if(_selectName == null){
_clickSpec();
return;
}
routePush(ConfirmOrderPage(productInfoBean: model.productInfoBean,norms: norms,count: _specModel.countNumber));
},
);
}
Widget _activityImage(){
return Container(
width: double.infinity,
height: dp80,
child: Image.asset(ImagePath.goodsActivity),
);
}
Widget _topTabTitle(){
return Container(
height: dp60,
child: TabBar(
controller: _tabController,
labelColor: ColorConfig.themeGreen,
unselectedLabelColor: ColorConfig.C09,
labelStyle: StyleConfig.green_36Style,
unselectedLabelStyle: StyleConfig.normalTitle36Style,
indicatorColor: ColorConfig.themeGreen,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 2,
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: dp20),
tabs: tabList.map((item) {
return Tab(
text: item,
);
}).toList(),
onTap: (index){
switch(index){
case 0:
_scrollController.jumpTo(0);
_tabController.animateTo(0);
break;
case 1:
_scrollController.jumpTo(twoY - oneY);
_tabController.animateTo(1);
break;
case 2:
_scrollController.jumpTo(threeY - oneY);
_tabController.animateTo(2);
break;
}
},
),
);
}
void _showPopup(){
showPopupWindow(
context,
gravity: KumiPopupGravity.rightTop,
underStatusBar: true,
underAppBar: true,
offsetX: -dp30,
offsetY: -dp20,
childFun: (pop){
return Container(
key: GlobalKey(),
child: GoodsDetailPopupMenuWidget(),
);
}
);
}
void _clickSpec(){
_specModel.initData(model.normInfoBean.norms);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return GoodsSpecSelectDialog(
model: _specModel,
onSelected: (bean, count) {
norms = bean;
setState(() {
_selectName = "已选:"+bean.normName + ",数量:" + count.toString();
});
},
);
},
);
}
#override
void dispose() {
_specModel.dispose();
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
}
Your global keys are attached to the widgets in _body() method but this method is just called when model.busy = false and model.error = false. It means globalKeyOne.currentContext will be null when model.busy = true || model.error = true. Your _afterLayout(...) is called in all the cases, that's why it fails with NPE.
use widgetsbinding.instance.addpostframecallback
if you put the method getPosition in the widgetsbinding..... then remember to add this in the parameter of getPosition(_)
for some reason this works
Here is the Sample Code
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
GlobalKey _akey = GlobalKey();
var top;
var left;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 300,
key: _akey,
color: Colors.blue,
),
),
);
}
_afterLayout(_) {
_getPosition();
}
_getPosition() async {
final RenderBox rb = _akey.currentContext.findRenderObject();
var position = rb.localToGlobal(Offset.zero);
top = position.dy;
left = position.dx;
print('$left , $top');
}
}
try putting it in build Function not initState()

play audio file from where i pause, not from the beginning in Audioplayers flutter package

i am new in flutter ... I want to play audio file from where i pause, not from the beginning in audioplayers flutter package ...
in this example there is just:
1- play from the beginning
2- pause
3- stop
pause and stop they have same function because by pause i can not play from where i pause ...
so what i want is to play from where i pause, in same button!
the audioplayers package which I use
https://pub.dev/packages/audioplayers
my code ...
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
typedef void OnError(Exception exception);
void main() {
runApp(new MaterialApp(debugShowCheckedModeBanner: false,home: LocalAudio()));
}
class LocalAudio extends StatefulWidget {
#override
_LocalAudio createState() => _LocalAudio();
}
class _LocalAudio extends State<LocalAudio> {
Duration _duration = new Duration();
Duration _position = new Duration();
AudioPlayer advancedPlayer;
AudioCache audioCache;
#override
void initState() {
super.initState();
initPlayer();
}
void initPlayer() {
advancedPlayer = new AudioPlayer();
audioCache = new AudioCache(fixedPlayer: advancedPlayer);
advancedPlayer.durationHandler = (d) => setState(() {
_duration = d;
});
advancedPlayer.positionHandler = (p) => setState(() {
_position = p;
});
}
String localFilePath;
Widget _tab(List<Widget> children) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.all(16.0),
child: Column(
children: children
.map((w) => Container(child: w, padding: EdgeInsets.all(6.0)))
.toList(),
),
),
],
);
}
Widget _btn(String txt, VoidCallback onPressed) {
return ButtonTheme(
minWidth: 48.0,
child: Container(
width: 150,
height: 45,
child: RaisedButton(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
child: Text(txt),
color: Colors.pink[900],
textColor: Colors.white,
onPressed: onPressed),
),
);
}
Widget slider() {
return Slider(
activeColor: Colors.black,
inactiveColor: Colors.pink,
value: _position.inSeconds.toDouble(),
min: 0.0,
max: _duration.inSeconds.toDouble(),
onChanged: (double value) {
setState(() {
seekToSecond(value.toInt());
value = value;
});
});
}
Widget LocalAudio() {
return _tab([
_btn('Play', () => audioCache.play('disco.mp3')),
_btn('Pause', () => advancedPlayer.pause()),
_btn('Stop', () => advancedPlayer.stop()),
slider()
]);
}
void seekToSecond(int second) {
Duration newDuration = Duration(seconds: second);
advancedPlayer.seek(newDuration);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 1,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 1.0,
backgroundColor: Colors.teal,
title: Center(child: Text('LOCAL AUDIO')),
),
body: TabBarView(
children: [LocalAudio()],
),
),
);
}
}
You can use the resume method to play the audio from where you paused it.
await audioPlayer.resume();
It only works if the audioPlayer state is Paused.
From the package documentation:
Also, you can resume (like play, but without new parameters):
int result = await audioPlayer.resume();
there is a function in class AudioPlayer for resume
it is look like this in class AudioPlayer
Future<int> resume() async {
final int result = await _invokeMethod('resume');
if (result == 1) {
state = AudioPlayerState.PLAYING;
}
return result;
}
to use it add
_btn('resume', () => advancedPlayer.resume()),
below line of
_btn('Pause', () => advancedPlayer.pause()),