Updating a Slider based on video position - flutter

Summarize the problem
I need help finding a Flutter construct which will allow me to repeatedly check the return value of an API function call for getting the current time of a playing video (position). My goal is to update the Slider() to the position of the current video. I am using plugin flutter_vlc_player for playing back videos.
The most important part of the code below is the videoPlayerController.getPosition() function call. I need a way to repeatedly call this function and get the latest value. This is what I am struggling with.
The SeekBar class instantiated is the Slider() I am updating.
I think I am close to a solution as StreamBuilder is meant to update based on events. Also if I perform hot refresh of app after playing a video, the Slider updates once.
What I am seeing is the stream function is called twice but returns null each time because the video isn't playing yet. I need the stream function to be called while the video is playing.
I/flutter (29465): snapshot: null
I/flutter (29465): snapshot: null
One last thing: videoPlayerController.getPosition() is a Future.
Describe what you've tried:
I tried using StreamBuilder() and FutureBuilder() but I got the same results. The current position is only fetched twice when I need it to be continuously fetched during video playback. I checked the Flutter documentation on StreamBuilder but their example only shows when there is one item to be grabbed and not multiple. I need to rebuild the Slider() widget based on the value returned from function repeatedly.
Show some code:
VlcPlayerController videoPlayerController = VlcPlayerController.network(
'rtsp://ip_addr:8554/test',
hwAcc: HwAcc.FULL,
autoPlay: true,
options: VlcPlayerOptions(
video: VlcVideoOptions(),
rtp: VlcRtpOptions(['--rtsp-tcp'],),
extras: ['--h264-fps=30']
),
);
await Navigator.push(context,
MaterialPageRoute(builder: (context) =>
Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.blueAccent,
title: const Text("Playback")),
body: Center(
child: Column(
children: [
VlcPlayer(
controller: videoPlayerController,
aspectRatio: 16/9,
placeholder: const Center(child: CircularProgressIndicator()),
),
StatefulBuilder(builder: (context, setState) {
return Row(
children: [
TextButton(
child: Icon(isPlaying ? Icons.play_arrow : Icons.pause),
style: ButtonStyle(backgroundColor: MaterialStateProperty.all<Color>(Colors.blueAccent),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white)),
onPressed: () {
setState(() {
if(videoPlayerController.value.isPlaying)
{
isPlaying = true;
videoPlayerController.pause();
}
else {
isPlaying = false;
videoPlayerController.play();
}
});
}
),
Text("${videoPlayerController.value.position.inMinutes}:${videoPlayerController.value.position.inSeconds}",
style: const TextStyle(color: Colors.white)),
],
);
}),
StreamBuilder<Duration>(
stream: Stream.fromFuture(videoPlayerController.getPosition()),
builder: (BuildContext context, AsyncSnapshot <Duration> snapshot) {
Duration position = snapshot.data ?? const Duration();
print('snapshot: ${snapshot.data?.inSeconds.toDouble()}');
return Column(
children: [
SeekBar(
duration: const Duration(seconds: 5),
position: position,
onChangeEnd: (newPosition)
{
videoPlayerController.seekTo(newPosition);
},
)
],
);
}
),
]
)
),
)
)
);
Thank you for reading and help. I am still learning Flutter/Dart so any references to helpful classes will be great.

Is there a reason you want to use StreamBuilder in this case? You can use a StatefulWidget and add a listener to the controller. Then update the position inside that listener.
Use this to add listener:
videoPlayerController.addListener(updateSeeker);
Also make sure to remove the listener in dispose method:
videoPlayerController.removeListener(updateSeeker);
Here is the updateSeeker method:
Future<void> updateSeeker() async {
final newPosition = await videoPlayerController.getPosition();
setState(() {
position = newPosition;
});
}
Here is an example of a widget that plays the video and shows its position in a Text widget:
class VideoPlayer extends StatefulWidget {
const VideoPlayer({Key? key}) : super(key: key);
#override
_VideoPlayerState createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
final videoPlayerController = VlcPlayerController.network(url);
var position = Duration.zero;
#override
void initState() {
super.initState();
videoPlayerController.addListener(updateSeeker);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
VlcPlayer(
aspectRatio: 16 / 9,
controller: videoPlayerController,
),
Text(position.toString()),
],
),
);
}
Future<void> updateSeeker() async {
final newPosition = await videoPlayerController.getPosition();
setState(() {
position = newPosition;
});
}
#override
void dispose() {
videoPlayerController.removeListener(updateSeeker);
super.dispose();
}
}

Related

Flutter Looping Endlessly when adding a FutureBuilder Inside another FutureBuilder

I have a Stop Watch timer in a Flutter Screen which works as required in the beginning inside a FutureBuilder but when I add another futurebuilder to add information from an API it keeps looping indefinitely:
Initially when I opened the screen it was looping indefinelty then when I adjusted the code it looped endlessly only when I clicked the Start button, it continued looping even after clicking the stop button.
Here is the dart file:
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
bool isStartButtonDisabled = false;
bool isStopButtonDisabled = true;
Stopwatch _stopwatch = Stopwatch();
String _elapsedTime = "00:00:00";
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
setState(() {
isStartButtonDisabled = true;
isStopButtonDisabled = false;
});
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
setState(() {
_elapsedTime = _stopwatch.elapsed.toString().split(".")[0];
});
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Here is ui
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
GFButton(
onPressed: isStartButtonDisabled ? null : startWorkout,
text: "Start Workout",
),
Text(
_elapsedTime,
),
Card(
child: Padding(
padding: const EdgeInsets.all(18.0), //change here
child: Column(
children: [
FutureBuilder<List<Model_No_1>>(
future: futureModel_No_1,
builder: (BuildContext context,
AsyncSnapshot<List<Model_No_1>> snapshot) {
if (snapshot.hasData) {
return Column(
children: List.generate(snapshot.data!.length,
(int index) {
String Some_VariableName =
snapshot.data![index].name;
return Column(
children: [
Text(
snapshot.data![index].name,
),
Builder(builder: (context) {
return Container(
child: SingleChildScrollView(
child: Column(
children: [
Card(
child: Row(
children: [
Expanded(
child: FutureBuilder<
Get_Old_Model_No>(
future: () {
final Map<String,dynamic> arguments =ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final int id =arguments['id'] ??
0;print("This is the id $id");return APIService.Get_Old_Model_No(id);}(),builder:(context, snapshot) {print("Snapshot data:${snapshot.data}");print("Snapshot error:${snapshot.error}");
if (snapshot.hasData) {
return Column(
children: [
// Text(snapshot
// .data
// ?.endDate),
],
);
} else if (snapshot
.hasError) {
return Text(
"${snapshot.error}");
}
return CircularProgressIndicator();
},),),],),),
for (var breakdown in snapshot.data![index].breakdowns)
Form(child: Expanded(child: Column(children: [TextFormField(
keyboardType:TextInputType.number,
onChanged:(value) {
final int?parsedValue =
int.tryParse(value);if (parsedValue !=
null) {setState(
() {variable1 =parsedValue;});} else {}
},),],),),),
When I add the Expanded, which runs the APIService.Get_Old_Model_No(id);
My question is why is the indefinete looping happening? How can I fix it ?
i cannot reproduce your problem from your provided code, but having multiple encapsuled futureBuilders shouldn't be a problem.
Any chance that the setState gets called from within one of your future(Builder)s?
That would trigger a rebuild, reloading the future and then setting the state again, triggering a rebuilt, and so on
When you call setState the build method is called, this triggers the FutureBulder to rebuild, thereby calling the future again.
What I see happening in your case is that once the _startStopwatch is called, the timer is started and it calls setState every second. I can't see your stop function, but it seems you are not canceling the Timer.periodic when you stop the stopWatch.
To solve your problem, you need to manage your state in such a way that only the necessary widget rebuild when the state changes. To achieve this you will need more than setState. See a simple example below using ValueNotifier and ValueListenableBuilder.
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ValueNotifier<bool> isStartButtonDisabled = ValueNotifier(false);
ValueNotifier<bool> isStopButtonDisabled = ValueNotifier(true);
Stopwatch _stopwatch = Stopwatch();
ValueNotifier<String> _elapsedTime = ValueNotifier("00:00:00");
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
isStartButtonDisabled.value = true;
isStopButtonDisabled.value = false;
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
_elapsedTime.value = _stopwatch.elapsed.toString().split(".")[0];
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Now for the widgets that need to rebuild when the state changes, just use ValueListenableBuilder, See below.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
ValueListenableBuilder<bool>(valueListenable: isStartButtonDisabled, builder: (context, value, child)=> GFButton(
onPressed:value ? null : startWorkout,
text: "Start Workout",
)),
ValueListenableBuilder<String>(valueListenable: _elapsedTime, builder: (context, value, child)=> Text(
value,
)),
In this way when _elapsedTime changes, only the Text widget is rebuilt, similarly, when isStartButtonDisabled changes, only GFButton is rebuilt.
Also remember to cancel the Timer when you stop the StopWatch.
You are defining and calling a function in your build method with is getting called every time you set state, which is occuring at every tick of your timer.
() {
final Map<String,dynamic> arguments = ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final
int id =arguments['id'] ?? 0;
print("This is the id $id");
return APIService.Get_Old_Model_No(id);
}()

Boolean value getting changed for all - Flutter Provider

I'm building a music player, there are two screens - At first screen, list of songs are present in ListView builder and second screen plays the respective music (player screen)
When I play music from the player screen by clicking the play button, the icon changed to pause but the problem is that for other player screen, it also get changed
I'm trying to achieve this using Flutter provider package, I think what happens is that isPlaying boolean is acting up as a global variable in provider class and all of them are consuming the same value, how to tackle this?
//Provider class
class MusicPlayerProvider extends ChangeNotifier {
final AudioPlayer audioPlayer = AudioPlayer();
bool isPlaying = false;
void playMusic(Map<String, dynamic> songMap) async {
await audioPlayer.play(UrlSource(songMap['link']));
isPlaying = true;
notifyListeners();
}
void stopMusic() async {
await audioPlayer.pause();
isPlaying = false;
notifyListeners();
}
}
// Music Listing Screen
class _MusicListScreenState extends State<MusicListScreen> {
List<Map<String, dynamic>> songList = [{'name': 'Helix', 'link': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', 'singer': 'Alan Walker'}, {'name': 'Lucifer', 'link': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3', 'singer': 'John Smith'}];
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: songList.length,
itemBuilder: (BuildContext context, int index){
return ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.grey
),
onPressed: (){
Navigator.push(context, MaterialPageRoute(builder: (context) => MusicPlayerScreen(songMap: songList[index])));
},
child: Text('Go to Player Screen', style: GoogleFonts.oswald(color: Colors.white))
)
},
);
}
}
// Music Player Screen
class MusicPlayerScreen extends StatefulWidget {
final Map<String, dynamic> songMap;
const MusicPlayerScreen({Key? key, required this.songMap}) : super(key: key);
#override
State<MusicPlayerScreen> createState() => _MusicPlayerScreenState();
}
class _MusicPlayerScreenState extends State<MusicPlayerScreen> {
#override
Widget build(BuildContext context) {
return Consumer<MusicPlayerProvider>(
builder: (BuildContext context, value, Widget? widgetValue){
return InkWell(
onTap: (){
if(value.isPlaying){
value.stopMusic();
} else {
value.playMusic(widget.songMap);
}
},
child: Container(
height: 50.0,
width: 50.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[300]
),
// Icon changed for other music player screen to - problem
child: Icon(value.isPlaying ? Icons.pause_circle_filled : Icons.play_circle_fill, color: Colors.white, size: 35.0),
),
),
},
);
}
}
In your provider you can track the song that is playing instead of just isPlaying bool. This way you can tell which song is playing (if any) and show play/pause button accordingly.
UPD: also I would recommend moving song list to provider as well. So screens would just display a state and provider would handle the data.

Flutter carousel Slider for dynamic videos

I am trying to make application in which a gridview show videos from local storage folder, and clicking on any video open slider and play the video. By swiping left or right, play next or previous video, for this purpose
I am using Carousel Slider package to implement sliding effect for dynamic video data, for playing video i am using Video player package
Code for Slider class is below
class VideoSlider extends StatefulWidget {
const VideoSlider({
Key? key,
required this.listOfVideos, // Contains all videos in folder
required this.initialIndex, // initial index used in carousel options to show current page
}) : super(key: key);
final List<String> listOfVideos;
final int initialIndex;
#override
_VideoSliderState createState() => _VideoSliderState();
}
class _VideoSliderState extends State<VideoSlider> {
List<VideoPlayerController> controller = []; // list to contain controller for all files in floder
int i = 0; // used in carousel items to specify controller list index
#override
void initState() {
super.initState();
// used for loop to add controller for all files in controller list
for (int j = 0; j < widget.listOfVideos.length; j++) {
controller.add(
VideoPlayerController.file(File(widget.listOfVideos[j]))
..initialize().then((value) {
setState(() {});
}),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CarouselSlider(
items: widget.listOfVideos.map((e) {
return controller[i].value.isInitialized
? AspectRatio(
aspectRatio: controller[i].value.aspectRatio,
child: VideoPlayer(controller[i]))
: Container(),}).toList(),
options: CarouselOptions(
height: double.infinity,
viewportFraction: 1,
initialPage: widget.initialIndex,
enlargeCenterPage: true,
enableInfiniteScroll: false,
), ),
// fab to play and pause video
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
From where i am calling VideoSlider
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return VideoSlider(
listOfVideos: listOfFiles,
initialIndex: index,
);
}),
);
Code display slider with exact number, but every slide show first video(video at zero index) not appropriate.Any solution thanks alot.

Is there a way to precache Lottie animations in flutter?

I have a lottie animation that I'm using as a loading screen:
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lottie/lottie.dart';
class Loading extends StatelessWidget {
final String loadingText;
Loading({this.loadingText});
Widget build(BuildContext context) {
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (loadingText != null) _buildLoadingText(loadingText),
_buildAnimation(),
],
),
);
}
Widget _buildLoadingText(String text) {
return Text(
loadingText,
style: GoogleFonts.poppins(
textStyle:
TextStyle(fontWeight: FontWeight.w500, color: Colors.black)),
);
}
Widget _buildAnimation() {
return Lottie.asset('assets/lottie/heartbeat_loading.json',
width: 300, repeat: true, animate: true);
}
}
And I'm using it like this for when my app initially loads:
_determineHome() {
return StreamBuilder(
stream: AppBlocContainer().authenticationBloc().loggedIn,
builder: (context, AsyncSnapshot<AuthenticationStatus> snapshot) {
// return Loading();
return AnimatedSwitcher(
duration: Duration(seconds: 2),
child: !snapshot.hasData
? Loading(
loadingText: 'Loading...',
)
: _buildSecondChild(snapshot.data));
},
);
This works, except that the lottie animation is being loaded maybe a second or two too late, which by the time the lottie asset loads, it's too late and the page has already transitioned.
I was wondering since I did was able to precache my SVG images in my main() by doing this:
Future.wait([
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/login.svg'),
null,
),
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/logo.svg'),
null,
),
precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoder, 'assets/svg/signup_panel_1.svg'),
null,
)
]);
Would I be able to do the same thing with lottie?
what I found is that lottie package in flutter, itself got a built in auto cache.(want it or not it will cache the animation into ram)
method A(own made):
so you basically need to call Lottie.asset('assets/Logo.json') once and the second time you will be able to load it without delay by calling Lottie.asset('assets/Logo.json') again because it was cached at the first time.
so this can be used as a cheat cache method, for example you can load it first in splash screen like below:
either by wrapping with a SizedBox and giving 0 height/width or using Visibility widget with visible property of false but maintainState to true, so its not visible but still will be in widget tree and load into ram.
Visibility(
visible: false,
maintainSize: false,
maintainState: true,
maintainAnimation: false,
maintainInteractivity: false,
child: Row(
children: [
Lottie.asset('assets/Logo.json'),
],
),
),
next time where you need just call Lottie.asset('assets/Logo.json') and it will load without delay.
method B:
this is mentioned by a guy called Pante on github.
class LottieCache {
final Map<String, LottieComposition> _compositions = {};
/// Caches the given [LottieAsset]s.
Future<void> add(String assetName) async {
_compositions[assetName] = await AssetLottie(assetName).load();
}
Widget load(String assetName, Widget fallback) {
final composition = _compositions[assetName];
return composition != null ? Lottie(composition: composition) : fallback;
}
}
there are 2 ways here:
1)use AssetLottie(assetName).load() method; provide the LottieCache object in order to access its load method where ever you need.
2)completly neglect load method and the composition way(you can comment the load method);because AssetLottie(assetName).load() loads the animation into ram just as the same as Lottie.asset('assets/Logo.json')
so just calling its add method will load animation into ram and you can use it without delay next time where you need.
method C:
this is a custom loading method mentioned in lottie documentation for flutter.
class _MyWidgetState extends State<MyWidget> {
late final Future<LottieComposition> _composition;
#override
void initState() {
super.initState();
_composition = _loadComposition();
}
Future<LottieComposition> _loadComposition() async {
var assetData = await rootBundle.load('assets/LottieLogo1.json');
return await LottieComposition.fromByteData(assetData);
}
#override
Widget build(BuildContext context) {
return FutureBuilder<LottieComposition>(
future: _composition,
builder: (context, snapshot) {
var composition = snapshot.data;
if (composition != null) {
return Lottie(composition: composition);
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
}

How to implement a Slider within an AlertDialog in Flutter?

I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()