MediaState stream ConnectionState stays on waiting - audio_service flutter - just-audio

I'm trying to provide my audioHandler on my Player class, but something weird is happening
When I enter the screen, the StreamBuilder will go active just fine but if i pop and navigate to the screen again the stream connect will stay on 'waiting' forever, unless i play the audio. This causes some weird behaviors. What m i doing wrong?
relevant code
Player class
final audioHandlerProvider = Provider<AudioHandler>((ref) {
AudioHandler _audioHandler = ref.read(audioHandlerServiceProvider);
return _audioHandler;
});
class _PlayerClicVozzState extends State<PlayerClicVozz> {
#override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Color(0xff131313),
appBar: AppBar(
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: Icon(Icons.clear, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Center(
child: Consumer(builder: (context, watch, child) {
final res = watch(audioHandlerProvider);
return StreamBuilder<MediaState>(
stream: _mediaStateStream(res),
builder: (context, snapshot) {
final mediaState = snapshot.data;
return SeekBar(
duration: mediaState?.mediaItem?.duration ?? Duration.zero,
position: mediaState?.position ?? Duration.zero,
onChangeEnd: (newPosition) {
res.seek(newPosition);
},
);
},
);
...
audioservice init
late AudioHandler _audioHandler;
final audioHandlerServiceProvider = Provider<AudioHandler>((ref) {
return _audioHandler;
});
Future<void> main() async {
_audioHandler = await AudioService.init(
builder: () => AudioPlayerHandler(),
config: AudioServiceConfig(
androidNotificationChannelId: 'com.mycompany.myapp.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: true,
),
);
...
My audiohandler is exatcly the same as the plugin example
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
class AudioPlayerHandler extends BaseAudioHandler with SeekHandler {
static final _item = MediaItem(
id: 'https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3',
album: "Science Friday",
title: "A Salute To Head-Scratching Science",
artist: "Science Friday and WNYC Studios",
duration: const Duration(milliseconds: 5739820),
artUri: Uri.parse(
'https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg'),
);
final _player = AudioPlayer();
/// Initialise our audio handler.
AudioPlayerHandler() {
// So that our clients (the Flutter UI and the system notification) know
// what state to display, here we set up our audio handler to broadcast all
// playback state changes as they happen via playbackState...
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
// ... and also the current media item via mediaItem.
mediaItem.add(_item);
// Load the player.
_player.setAudioSource(AudioSource.uri(Uri.parse(_item.id)));
}
// In this simple example, we handle only 4 actions: play, pause, seek and
// stop. Any button press from the Flutter UI, notification, lock screen or
// headset will be routed through to these 4 methods so that you can handle
// your audio playback logic in one place.
#override
Future<void> play() => _player.play();
#override
Future<void> pause() => _player.pause();
#override
Future<void> seek(Duration position) => _player.seek(position);
#override
Future<void> stop() => _player.stop();
/// Transform a just_audio event into an audio_service state.
///
/// This method is used from the constructor. Every event received from the
/// just_audio player will be transformed into an audio_service state so that
/// it can be broadcast to audio_service clients.
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
MediaControl.rewind,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.fastForward,
],
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: const {
ProcessingState.idle: AudioProcessingState.idle,
ProcessingState.loading: AudioProcessingState.loading,
ProcessingState.buffering: AudioProcessingState.buffering,
ProcessingState.ready: AudioProcessingState.ready,
ProcessingState.completed: AudioProcessingState.completed,
}[_player.processingState]!,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
);
}
}
MediaStateStream and QueueStateStream
Stream<MediaState> _mediaStateStream(AudioHandler audioHandler) {
return Rx.combineLatest2<MediaItem?, Duration, MediaState>(
audioHandler.mediaItem,
AudioService.position,
(mediaItem, position) => MediaState(mediaItem, position));
}
_queueStateStream(AudioHandler audioHandler) {
return Rx.combineLatest2<List<MediaItem>?, MediaItem?, QueueState>(
audioHandler.queue,
audioHandler.mediaItem,
(queue, mediaItem) => QueueState(queue, mediaItem));
}

When you subscribe to a stream, you only start receiving new events that are emitted after the moment that you subscribe, and you may have a period of waiting for that next event.
In your implementation of _mediaStateStream you are making use of AudioService.position which only emits events when the position is changing (i.e. not paused or stalled). So even though the stream may have emitted position events in the past, if you subscribe to that stream again while paused or stalled, you will be in a waiting state until the next position event arrives which is after playback resumes again.
I would suggest wrapping your stream in rxdart's BeehaviorSubject so that it retains a memory of the last event and re-emits the last event to new listeners. Also, you could seed this BehaviorSubject with the very first value to ensure there is no waiting period even for the first listener:
_mediaStateSubject = BehaviorSubject.seeded(MediaState(
handler.mediaItem.valueOrNull,
handler.playbackState.position))
..addStream(_mediaStateStream(handler));
Then you can listen to _mediaStateSubject instead of _mediaStateStream.

Related

Flutter with SQFlite error on pushing to database

I convert the data from my class instance to a map then pass that map into a database which I think is set up correctly but am getting the following error:
Tried calling: insert(conflictAlgorithm: Instance of 'ConflictAlgorithm', data: _LinkedHashMap len:5, table: "sessionchunk")
Found: insert(String, Map<String, Object?>, {String? nullColumnHack, ConflictAlgorithm? conflictAlgorithm}) => Future<int>
heres my code:
globals.dart (bad file name, more of a database helper file)
library data_practice.globals;
import 'models/sessionModel.dart';
import "package:sqflite/sqflite.dart";
import "package:path/path.dart";
var database;
Future<Database> initDatabase() async {
return database = openDatabase(
// path to the database
join(await getDatabasesPath(), 'user_data.db'),
// database version
version: 1,
// on create
onCreate: (db, version) {
return db.execute(
"CREATE TABLE sessionchunk(id INTEGER PRIMARY KEY, worktime INTEGER, breaktime INTEGER, intention TEXT, progress TEXT)");
});
}
// create an instance of this to get access to the db
pushChunk(SessionChunk chunk) async {
//create instance of the mysql database, this might cause issues
//by having multiple instances??
var db = await database;
//takes table name,
await db.insert(
table: 'sessionchunk',
data: chunk.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
sessionModel.dart (this is the main model that I'm trying to store into the sessionchunk table)
import "package:path/path.dart";
import "package:sqflite/sqflite.dart";
import "package:data_practice/main.dart";
import "package:data_practice/globals.dart" as globals;
class SessionChunk {
int id = 0;
int workTime = 40;
int breakTime = 10;
String intention = "finish backend";
String progress = "done";
SessionChunk({required id, worktime, breakTime, intention, progress});
// helper method: converts chunk data into map for sqflite db
Map<String, dynamic> toMap() {
return {
"id": this.id,
"worktime": this.workTime,
"breaktime": this.breakTime,
"intention": this.intention,
"progress": this.progress,
};
}
// helper method: prints current values of refrenced session instance
#override
String toString() {
return 'SessionChunk{id: $id, workTime: $workTime, breakTime: $breakTime, intention: $intention, progress: $progress}';
}
}
then my main.dart file:
import 'package:flutter/material.dart';
import "package:path/path.dart";
import "package:sqflite/sqflite.dart";
import "./models/sessionModel.dart";
import 'globals.dart' as globals;
//TODO: left off at step 5 in on https://docs.flutter.dev/cookbook/persistence/sqlite
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Database db = await globals.initDatabase();
print(db);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Push to database, press below',
),
TextButton(
onPressed: () {
var chunk = SessionChunk(
id: 1,
);
globals.pushChunk(chunk);
},
child: Text("push"),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Any help with how doing this would be greatly appreciated, pretty new to all this so please explain like Im 5! Happy Friday :)

How to passing data API with bottom navigation bar on flutter

I have an app that routes to a homescreen after login. This class contains a bottom navigation bar with 3 routes inside. body, forum, and profile. Inside body/ mainscreen i want to display an weather widget. I get repo code for add some weather widget. This weather widget need refresh/loading for initialize to get data from API, i want to put this loading class inside my Homescreen. but my problem is the bottom navigator bar not calling/showing on mainscreen/body. I think its only calling Get API DATA on this
Navigator.pushReplacementNamed(context, "/body", arguments: {
"weatherData": WeatherData.fromJson(data),
"selectedLocation": arguments
});
not Bottom navigation bar
_layoutPage = [Body(), Forum(), Profile()];
My question is How to merge bottom navigaton bar and this calling API data,
so every routes to body is calling loading to get api data and show bottom navigation bar.
this is my Homescreen
class HomeScreen extends StatefulWidget {
HomeScreen({Key key, this.title}) : super(key: key);
final String title;
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
#override
void initState() {
Provider.of<FirebaseOperations>(context, listen: false)
.initUserData(context);
super.initState();
}
final _layoutPage = [Body(), Forum(), Profile()];
void _onTapItem(int index) {
setState(() {
_selectedIndex = index;
});
}
// Weather
String apiKey = "MY API KEY";
SimpleLocationResult arguments;
getData({lat, lon}) async {
String latitude = lat == null ? "-6.2146" : lat.toString();
String longitude = lon == null ? "106.8451" : lon.toString();
Response response = await get(
"https://api.openweathermap.org/data/2.5/weather?units=metric&lat=$latitude&lon=$longitude&appid=$apiKey");
Map data = jsonDecode(response.body);
Navigator.pushReplacementNamed(context, "/body", arguments: {
"weatherData": WeatherData.fromJson(data),
"selectedLocation": arguments
});
}
#override
Widget build(BuildContext context) {
//loading weather
arguments = ModalRoute.of(context).settings.arguments;
Future.delayed(
Duration(seconds: 1),
() => {
arguments != null
? getData(lat: arguments.latitude, lon: arguments.longitude)
: getData()
});
return Scaffold(
body: _layoutPage.elementAt(_selectedIndex),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Colors.white,
selectedItemColor: kDarkGreenColor,
unselectedItemColor: kMainColor,
selectedLabelStyle:
TextStyle(color: kMainColor, fontFamily: 'Roboto', fontSize: 14.0),
unselectedLabelStyle: TextStyle(
color: Colors.grey[600], fontFamily: 'Roboto', fontSize: 12.0),
showUnselectedLabels: true,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(EvaIcons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(EvaIcons.messageSquare), label: 'Forum'),
BottomNavigationBarItem(
icon: Icon(EvaIcons.person), label: 'Profile'),
],
currentIndex: _selectedIndex,
onTap: _onTapItem,
),
);
}
}

How to dynamically populate MediaItem from firestore in audio_service?

I have been trying to dynamically populate MediaItem with some audio data from firestore.
I am using the exact plugin example, but this time mediaItems is being sourced dynamically from firestore. I have reviewed my code multiple times, but I can't figure out what I am doing wrong.
Here are my attempts:
First I fetched the song data using a StreamBuilder and passed it as a DocumentSnapshot List to the AudioServicePlayer() page.
List<DocumentSnapshot> _list;
_list = snapshot.data.docs;
Flexible(
child: ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => JustAudioPlaylist(
songs: [_list[index]],
),
));
},
child:
Container(child: Center(child: Text('My Playlists'))),
);
}),
)
I successfully received the QueryDocumentSnapshot as expected. But when I tried to populate MediaItem with widget.songs List, it returns just a blank white page with no error. I can't figure out what I am doing wrong here;
class AudioServicePlayer extends StatefulWidget {
static const String id = 'audio-service';
List<DocumentSnapshot> songs = [];
AudioServicePlayer({this.songs});
#override
_AudioServicePlayerState createState() => _AudioServicePlayerState();
}
class _AudioServicePlayerState extends State<AudioServicePlayer> {
MediaLibrary _mediaLibrary = MediaLibrary();
#override
void initState() {
_mediaLibrary._items.addAll(widget.songs
.map((song) => MediaItem(
// This can be any unique id, but we use the audio URL for convenience.
id: song['song'],
album: "Science Friday",
title: song['songTitle'],
artist: song['artist']['artistName'],
duration: Duration(milliseconds: 5739820),
artUri: Uri.parse(song['songImage']).toString(),
))
.toList());
super.initState();
}

How to dynamically populate AudioSource with multiple audio data from firestore in just_audio?

I have been trying to dynamically populate AudioSource.uri() with data from firestore.
I uploaded some songs into firestore database and I wanted to use the data for a just_audio playlist in my app. I have done everything possible, and I really am not sure why its not working.
I don't want to add the song urls and other data statically as shown in the plugin example.
Here are my attempts:
First I fetched the song data using a StreamBuilder and passed it as a DocumentSnapshot List to the JustAudioPlaylist() page;
List<DocumentSnapshot> _list;
_list = snapshot.data.docs;
Flexible(
child: ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => JustAudioPlaylist(
songs: [_list[index]],
),
));
},
child:
Container(child: Center(child: Text('My Playlists'))),
);
}),
)
Then, here's the JustAudioPlaylist page where I expected to retrieve and populate the AudioSource.uri().
class JustAudioPlaylist extends StatefulWidget {
final List songs;
JustAudioPlaylist({this.songs});
#override
_JustAudioPlaylistState createState() => _JustAudioPlaylistState();
}
class _JustAudioPlaylistState extends State<JustAudioPlaylist> {
AudioPlayer _player;
int _addedCount = 0;
var _playlist;
#override
void initState() {
_playlist
.addAll(widget.songs.map((song) => ConcatenatingAudioSource(children: [
AudioSource.uri(
Uri.parse(song['song']),
tag: AudioMetadata(
album: "Science Friday",
title: song['songTitle'],
artwork: song['songImage'],
),
),
])));
I am not sure why its not working, but it produces an error "addAll was called on null". Please can anyone help?
Your relevant code is:
_playlist.addAll(...);
The error means _playlist is null. That is, _playlist is an uninitialised variable and doesn't actually contain any playlist object. I can see you declare the variable so it starts off empty:
var _playlist;
But you never actually store anything into this variable, like _playlist = ...something.... So your _playlist variable starts off null and continues to remain null.
You could do this instead:
_playlist = ConcatenatingAudioSource(children: []);
// and then later...
_playlist.addAll(widget.songs.map(...etc...));
Although addAll is intended for dynamically modifying the playlist after it's already created. But in your case, you know which songs you want to play at initialisation time, so you may as well just initialise the playlist right at the beginning and you won't have to add to it later:
_playlist = ConcatenatingAudioSource(
children: widget.songs.map(...etc...)
);

How to build a login page in flutter using path_provider package for storing login in local file

I have created a save method and read method for storing credentials.
In the code below username and password are going to be saved in a file.
Code is also giving the directory path where it is going to be stored but it is not working properly while tapping on the login button. The code represents the login screen which appears when we open the app for the first time. The user account login page will open after filling the credentials. It will never ask to the user for logins till the user clear the data or uninstall the app. In this code username and password are the text filed, their is a checkbox in the login screen which will act as a remember me box. It will call the save method on click and now I want that checkbox will save credentials only after pressing the login button.
class LoginPageState extends State<LoginPage> {
TextEditingController username = new TextEditingController();
TextEditingController password = new TextEditingController();
bool checkValue = false;
String user ="username";
String pass="password";
#override
void initState() {
super.initState();
// call credentials method
readuser().then((String value) {
setState(() {
user="username";
user = value;
});
});
readpass().then((String value) {
setState(() {
pass="password";
pass = value;
});
});
}
bool checkingdata() {
if (user.length !=0 && pass.length != 0) {
return true;
} else {
return false;
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
elevation: 0.0,
backgroundColor: Colors.white12,
),
body: new SingleChildScrollView(
child: _body(),
scrollDirection: Axis.vertical,
),
);
}
Widget _body() {
return new Container(
padding: EdgeInsets.only(right: 20.0, left: 20.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Container(
margin: EdgeInsets.all(30.0),
child: new Image.asset(
"assets/images/flutter_icon.png",
height: 100.0,
),
),
new TextField(
onSubmitted: (_) {
setState(() {
user = username.text;``
});
},
controller: username,
decoration: InputDecoration(
hintText: "username",
hintStyle: new TextStyle(color: Colors.grey.withOpacity(0.3))),
),
new TextField(
controller: password,
obscureText: true,
onSubmitted: (_) {
setState(() {
pass = password.text;
});
},
decoration: InputDecoration(
hintText: "password",
hintStyle:
new TextStyle(color: Colors.grey.withOpacity(0.3)))),
new CheckboxListTile(
value: checkValue,
title: new Text("Remember me"),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (bool value) { checkingdata();
_saved(user, pass);
},
),
new Container(
decoration:
new BoxDecoration(border: Border.all(color: Colors.black)),
child: new ListTile(
title: new Text(
"Login",
textAlign: TextAlign.center,
),
onTap: _saved(user, pass),
),
)
],
),
);
}
//for username
_saved (String text,String text2) async {
print("asggd");
final userfile = await _localFile;
//print("asd");
// Write the file.
_savedpass(text2);
print("asded");
return userfile.writeAsString(text);
}
//for password
_savedpass(String text) async {
final passfile = await _localFile;
// Write the file.
return passfile.writeAsString(text);
}
Future<String> readuser() async {
try {
final userfile = await _localFile;
// Read the file.
String contents = await userfile.readAsString();
return contents;
} catch (e) {
// If encountering an error, return 0.
//return "dfd";
}
}
Future<String> readpass() async {
try {
final userpass = await _localFile;
// Read the file.
String contents = await userpass.readAsString();
return contents;
} catch (e) {
// If encountering an error, return 0.
//return;
}
}Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
var _localPath;
final path = await _localPath;
return File('$path/counter.txt');
}
}
Read function is used for the testing purposes. It will print the login credential on the home screen. I also want to add one more method in this code which is used to check the saved login credentials form the file so that next time when you reuse the app it will directly open the home screen.
Putting aside the fact that you want to store your credentials in plaintext in a local file, your problem is your _localFile getter. You define a local variable _localPath, so in the next line of code, all you're getting is null. Also, you probably want to create your file if it doesn't exist, simply by calling file.create().
you may not have the file counter.txt, so you need to create it first. I would advise you to use shared_preferences or hive to store your data.
For example with shared_preferences it will looks like in snippet below:
To retrieve data
final prefs = await SharedPreferences.getInstance();
final login = prefs.getString('login');
final password = prefs.getString('password');
// It's important to check it to the null
if (login != null && password != null) {
// Use it
}
To store new data
final prefs = await SharedPreferences.getInstance();
await prefs.setString('login', login);
await prefs.setString('password', password);
And you can remove from the preference as well:
final prefs = await SharedPreferences.getInstance();
await prefs.remove('login');
await prefs.remove('password');