How to cache network videos in flutter application? - firebase

Im trying to understand what caching videos means and how exactly it works.
The problem that I had was a high bandwidth in my flutter /firebase application . I had like 19gb a day with 10-20 videos and like up to 10 users. So I could not figure out what the problem was. Therefore I contacted firebase support and they say
Looking at the graph, the high bandwidth comes from the storage bucket where the videos are stored. Even though it looks like there are few videos, your bandwidth will increase more and more if your application doesn't store the videos in cache.
Try to double check your applications and ensure that these ones download the information only once.
And I was like what the hak is chaching? And how to do it ?
And will this solve the problem of high bandwidth?
here's how my code looks like
class Videoplayeritem extends StatefulWidget {
final bool mute;
final int pickedvideo;
final int currentPageIndex;
final bool isPaused;
final int pageIndex;
final String videourl;
final String thumbnailUrl;
const Videoplayeritem({
Key key,
this.videourl,
this.currentPageIndex,
this.isPaused,
this.pageIndex,
this.thumbnailUrl,
this.pickedvideo,
this.mute,
}) : super(key: key);
#override
_VideoplayeritemState createState() => _VideoplayeritemState();
}
class _VideoplayeritemState extends State<Videoplayeritem> {
VideoPlayerController videoPlayerController;
bool initialized = false;
bool stopvideo = false;
#override
void initState() {
super.initState();
try {
videoPlayerController = VideoPlayerController.network(
widget.videourl,
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
)..initialize().then((value) {
if (this.mounted) setState(() {});
try {
videoPlayerController?.play();
videoPlayerController?.setLooping(true);
if (widget.mute) {
videoPlayerController?.setVolume(0);
} else if (!widget.mute) {
videoPlayerController?.setVolume(1);
}
} catch (e) {
print('error: $e');
}
});
} catch (e) {
print('error2: $e');
}
print('init');
}
#override
void dispose() {
try {
if (videoPlayerController.value.isPlaying) {
videoPlayerController?.pause();
}
videoPlayerController?.setVolume(0);
videoPlayerController?.dispose();
videoPlayerController = null;
} catch (e) {
print('error3: $e');
}
print('dispose');
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.pageIndex == widget.currentPageIndex &&
!widget.isPaused &&
!stopvideo ||
widget.pageIndex == widget.pickedvideo &&
widget.currentPageIndex == null &&
!stopvideo) {
setState(() {
videoPlayerController?.play();
});
} else {
setState(() {
videoPlayerController?.pause();
});
}
if (widget.mute) {
videoPlayerController?.setVolume(0);
} else if (!widget.mute) {
videoPlayerController?.setVolume(1);
}
return Container(
color: Colors.black,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Center(
child: videoPlayerController.value.isInitialized
? GestureDetector(
onTap: () {
if (videoPlayerController.value.isPlaying) {
if (this.mounted) {
setState(() {
stopvideo = true;
videoPlayerController?.pause();
});
}
} else {
if (this.mounted) {
setState(() {
stopvideo = false;
videoPlayerController?.play();
videoPlayerController?.setLooping(true);
});
}
}
},
child: VisibilityDetector(
key: Key("unique keys"),
onVisibilityChanged: (VisibilityInfo info) {
debugPrint(
"${info.visibleFraction} of my widget is visible");
if (info.visibleFraction == 0) {
print("pause");
if (stopvideo == false) {
if (this.mounted) {
setState(() {
stopvideo = true;
});
}
}
videoPlayerController?.pause();
} else if (widget.pageIndex == widget.currentPageIndex ||
widget.pageIndex == widget.pickedvideo &&
widget.currentPageIndex == null) {
if (this.mounted) {
if (stopvideo == true) {
setState(() {
stopvideo = false;
});
}
}
videoPlayerController?.play();
} else {}
},
child: Stack(children: [
Center(
child: AspectRatio(
aspectRatio: videoPlayerController.value.aspectRatio,
child: VideoPlayer(videoPlayerController),
),
),
PlayPauseOverlay(
controller: videoPlayerController,
stopvideo: stopvideo,
)
]),
))
: Center(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CachedNetworkImage(
errorWidget: (context, url, error) => Icon(Icons.error),
imageUrl: widget.thumbnailUrl,
fit: BoxFit.cover,
),
),
)),
);
}
}
My app is playing videos in Preloadpageview which are vertical scrollable like reels in instagram. The videos are loaded from stream .
Hope anyone can explain what chaching exactly mean and how it will affect my high bandwidth . Also how to used it in my case ?

The problem that I had was a high bandwidth in my flutter /firebase application . I had like 19gb a day with 10-20 videos and like up to 10 users.
There are two layers where caching can with this problem: initial video download, and subsequent video replay.
For initial video download, one option is to dedicate a server to act as an intermediary cache. It would download and stay in sync with the current videourl's content and then serve it. The videourls would then be pointed to this server so the client pulls videos from it.
This only moves the problem around though, and bandwidth isn't free. But you don't have to host this cache server, there are companies that will host for a fee.
The way caching can help for subsequent video replay is by keeping it in local temporary storage on the video playing client, and when returning to the video, retrieving it from local temporary storage and playing it - thereby avoiding asking it from the server again.
One possibly quick solution could be by using the better_player library. It allows many configurations, including using a cache. You can find it here

I might be a little late to answer the question, but if you or anybody else out there are still looking for the answer here's how I did it.
After trying every possible library out there I ended up making my own "video cacher". Note: This might not be the prettiest or the slickest solution, but it gets the job done.
On my main file:
Homepage.dart
body: FutureBuilder(
future: FetchPostData,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return SizedBox(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return Container(
child: VideoCacher(snapshot.data!.docs[index].data()['VideoUrl'])))
I had a future builder using a cached future stream. Now the data I got form that stream I passed it into another file with a stateful widget called: VideoCacher.dart
VideoCacher.dart
#override
void initState() {
super.initState();
getMedia();
}
Future<void> getMedia() async {
String exp = r'(_.*\.(mp4))';
final RegExp regExp = RegExp(exp);
final String? fileName = regExp.stringMatch(widget._videoUrl);
final Directory tmpDir = Directory.systemTemp;
final File file = File('${tmpDir.path}/$fileName');
if (file.existsSync() == false) {
FirebaseStorage.instance
.ref()
.child('videos')
.child(fileName!)
.writeToFile(file);
setState(() {
_cachedFile = file;
});
} else {
setState(() {
_cachedFile = file;
});
}
}
#override
Widget build(BuildContext context) {
return PostVideoPlayer(_cachedFile);
}
}
In this file I created a tmp location on the system directory where I would download the film from the data I got in stream and pass it on to another file called: VideoPlayer.dart
VideoPlayer.dart
Here I would use the original video_player library provided by flutter team to output the video to my main file.
body: Stack(
alignment: Alignment.bottomCenter,
children: [
SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
child: SizedBox(
width: _controller.value.size.width,
height: _controller.value.size.height,
child: VideoPlayer(_controller),
),
),
),
],
),
So the chain would be: Homepage.dart < return VideoCacher.dart <
return VideoPlayer.dart
Prior to this I had my bandwidth consumption at hitting the daily limit in just couple of minutes. Even though I only had 3 videos and total storage of about 2mb.
What I noticed was that anytime you put anything that even remotely relies on fetching data or dynamic data you are bound to run into problems as build method is called every time there is a change. So if you try to get video from storage it will send thousands of requests in succession until you run out of bandwidth.
After this method my bandwidth consumption was around couple mbs, before the video was downloaded and 0mb after it was cached.
Let me know if you found this helpful.
Good Luck.

Related

Uploading picture in Firebase storage flutter dart

I am trying to upload pictures from my gallery, the code works perfectly and is running, but not on my device. I want to know why it isn't working with me please help
this is what is showing when I'm trying to run.
The selection of images is working too
name and title and all that is showing if Firebase but image are not visible in storage.
Future selectFile() async {
final result = await FilePicker.platform
.pickFiles(allowMultiple: false, type: FileType.any);
if (result != null) {
final path = result.files.single.path;
file = File(path!);
setState(() => file = File(path));
}
}
Future uploadFile() async {
if (file == null) return;
final fileName = basename(file!.path);
final destination = 'Content/$fileName';
task = FirebaseApi.uploadFile(destination, file!);
setState(() {});
}
Widget buildUploadStatus(UploadTask uploadTask) =>
StreamBuilder<TaskSnapshot>(
stream: task!.snapshotEvents,
builder: (context, snapshot) {
if (snapshot.hasData) {
final snap = snapshot.data!;
final progress = snap.bytesTransferred / snap.totalBytes;
final percentage = (progress * 100).toStringAsFixed(2);
return Row(
children: [
Text(
'$percentage %',
style: GoogleFonts.asap(
fontSize: 17,
color: Colors.white,
fontWeight: FontWeight.w500,
),
)
],
);
} else {
return Container();
}
},
);
#override
State<StatefulWidget> createState() {
// TODO: implement createState
throw UnimplementedError();
}
}
class StatfulWidget {}
class FirebaseApi {
static UploadTask? uploadFile(String destination, File file) {
try {
final ref = FirebaseStorage.instance.ref(destination);
return ref.putFile(file);
} on FirebaseException catch (e) {
return null;
}
}
}
That's because you're giving the database an empty value. I encountered a similar issue where I was sending the most recent message to the database and initializing it with "." Instead, use something like a space " ".
Example code:
admin.firestore().collection('whatever').doc(""+id);
or use ${id} surrounded with backticks `
An alternative solution could be that data needs to be in JSON format. By default, it is set to text, so it must be changed to JSON (application/json).
Here is a Github forum that is related to same issue: https://github.com/firebase/firebase-admin-node/issues/320

Flutter subscribe/query to one field in one file in cloud firestore

New to booth flutter and stackoverflow.
I am making the account verification functionally for my flutter app. My plan is to divided this functionally into two parts, part one shows an alertdialog when the screen is built, and part two checks if the "activated" field in firestore is true or false. I have problem of making part two.
This is what I write for part one
String uid = "fdv89gu3njgnhJGBh";
bool isActivated = false;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (isActivated == false) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: AlertDialog(
title: Text("Activation pending"),
content: Text("Your account is waiting to be activate by admin"),
actions: [
FlatButton(
child: Text("Refresh"),
onPressed: () {
// just bring reassurance to user
},
),
],
),
);
});
}
});
}
For part two I plan to make a Future return type function, what it will do is to subscribe the boolean value that stored in firestore: /user/uid/activated, once the function gets a "true" from firestore, it will return it to part one and part one will close the alertdialog(which I haven't figure out how to do this).
I've already seen some solutions from the internet but most solutions involve StreamBuilder, but it seems that I don't need to build any widget for the stream in part two. Is it better to just make changes to what I write previously* or integrate both parts two one StreamBuilder function?
*What I wrote for get the data from one field among all files (and this works well):
Future<bool> registeredCheck(String email) async {
var userInfo = await _firestore.collection("user").get();
for (var userInf in userInfo.docs) {
if (userInf.data()["email"] == email) {
return true;
}
}
return false;
}
}
Thank you
You don't have to query the entire collection. Since you already know the uid, you can just get the document of the uid directly like this:
Future<bool> registeredCheck(String email) async {
final userDoc = await _firestore.collection("user").doc(uid).get();
return userDoc.data()['activated'] ?? false;
}
The reason why I am adding ?? false is to return false instead of null when the activated value is null;

Adding up values stored in Firebase and displaying total in a FutureBuilder

Im trying to retrieve a list of monthly expenses from Firebase, add up the amount of each monthly expense and show it in a FutureBuilder.
In the Text widget i'm simply getting null. Been trying to google an answer for the past half hour but i don't even know where to begin as in what to search for.
Tried throwing in some print statement to see whats going on but any print statement I put in after the provider call doesn't show.
EDIT: Added Future casts to the commented lines. still no luck. but ignored the variable and called the future function directly from the FutureBuilder. Now all print statements are working and it is returning an instance of 'MonthlyExpense'. It is null still.
class SummaryView extends StatefulWidget {
#override
_SummaryViewState createState() => _SummaryViewState();
}
class _SummaryViewState extends State<SummaryView> {
/* Removed future variable and initState()
Future<double> _totalExpensesAmount; //Future<double> added
#override
void initState() {
super.initState();
print("init");
_totalExpensesAmount = _getTotalExpensesAmount();
}
*/
Future<double> _getTotalExpensesAmount() async { //Future<double> added
print("started");
final user = Provider.of<BPUser>(context);
print("user: $user");
double expensesTotal = 0.0;
var snapshot = await FirebaseFirestore.instance
.collection("BPUsers/${user.uid}/monthlyexpenses")
.get();
print(snapshot);
List<MonthlyExpense> searchedProducts =
DatabaseService().monthlyExpenseListFromSnapshot(snapshot);
print(searchedProducts);
for (var i = searchedProducts.length; i >= 1; i--) {
expensesTotal += double.parse(searchedProducts[i].amount);
}
return Future<double>expensesTotal; //Future<double> added
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: mainBackgroundColor,
body: Column(
children: [
Container(
child: FutureBuilder(
future: _getTotalExpensesAmount(), // called the function directly
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(
"${snapshot.data}",
style: nameStyle,
);
} else {
return Text("Loading...");
}
}),
)
],
),
);
}
}
The monthlyExpenseListFromSnapshot function (which works perfectly in another widget I use it in):
List<MonthlyExpense> monthlyExpenseListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return MonthlyExpense(
name: doc.data()['name'] ?? '',
amount: doc.data()['amount'] ?? '',
isActive: doc.data()['isActive']);
}).toList();
}
The firebase database:
You're getExpensesTotal doesn't return a future. Because of this, the widgetvwill never rebuild since the data is already calculated. FutureBuilder and StreamBuilder only cause a rebuild AFTER data been loaded after the initial build. Try surrounding your expensesTotal with Future.value.
You need to define the signature of the function to represent what it is expected, in this case Future <double>.
// ....
Future<double> _getTotalExpensesAmount() async {
First issue was to skip the variable and the init call and call the _getTotalExpensesAmount() future function (adding the Future signature to it).
Secondly my for loop was wrong. needed to change the conditions to:
for (var i = searchedProducts.length - 1; i >= 0; i--) {
Everything is working fine now!

How to cache Firebase data in Flutter?

In my app I build a list of objects using data from Firebase. Inside a StreamBuilder, I check if the snapshot has data. If it doesen't, I am returning a simple Text widget with "Loading...". My problem is that if I go to another page in the app, and then come back, you can see for a split second that it says 'Loading...' in the middle of the screen, and it is a bit irritating. I am pretty sure it is downloading the data from Firebase, and building the widget every time I come back to that page. And if I don't do the check for data, it gives me a data that I am trying to access data from null.
Is there a way to cache the data that was already downloaded, and if there has been no change in the data from Firebase, then just use the cached data?
Heres a redacted version of my code:
class Schedule extends StatefulWidget implements AppPage {
final Color color = Colors.green;
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
List<Event> events;
List<Event> dayEvents;
int currentDay;
Widget itemBuilder(BuildContext context, int index) {
// Some Code
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
events = new List(snapshot.data.documents.length);
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
events[i] = Event(
name: doc["name"],
start: DateTime(
doc["startTime"].year,
doc["startTime"].month,
doc["startTime"].day,
doc["startTime"].hour,
doc["startTime"].minute,
),
end: DateTime(
doc["endTime"].year,
doc["endTime"].month,
doc["endTime"].day,
doc["endTime"].hour,
doc["endTime"].minute,
),
buildingDoc: doc["location"],
type: doc["type"],
);
}
events.sort((a, b) => a.start.compareTo(b.start));
dayEvents = events.where((Event e) {
return e.start.day == currentDay;
}).toList();
return ListView.builder(
itemBuilder: itemBuilder,
itemCount: dayEvents.length,
);
},
),
);
}
}
You can use the the following code to define the source you want to retrieve data from. This will search either in local cache or on the server, not both. It works for all get() parameters, no matter if it is a search or document retrieval.
import 'package:cloud_firestore/cloud_firestore.dart';
FirebaseFirestore.instance.collection("collection").doc("doc").get(GetOptions(source: Source.cache))
To check if the search has data in cache, you need to first run the search against cache and if there is no result, run it against the server.
I found project firestore_collection to use a neat extension that can greatly simplify this process.
import 'package:cloud_firestore/cloud_firestore.dart';
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_document.dart
extension FirestoreDocumentExtension on DocumentReference {
Future<DocumentSnapshot> getSavy() async {
try {
DocumentSnapshot ds = await this.get(GetOptions(source: Source.cache));
if (ds == null) return this.get(GetOptions(source: Source.server));
return ds;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
}
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_query.dart
extension FirestoreQueryExtension on Query {
Future<QuerySnapshot> getSavy() async {
try {
QuerySnapshot qs = await this.get(GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return this.get(GetOptions(source: Source.server));
return qs;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
If you add this code, you can simply change the .get() command for both documents and queries to .getSavy() and it will automatically try the cache first and only contact the server if no data can be locally found.
FirebaseFirestore.instance.collection("collection").doc("doc").getSavy();
To be sure whether the data is coming from Firestore's local cache or from the network, you can do this:
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
print(doc.metadata.isFromCache ? "NOT FROM NETWORK" : "FROM NETWORK");
In the case you described you are probably going to still see the loading screen when its "NOT FROM NETWORK". This is because it does take some time to get it from the local cache. Soon you will be able to ask for the query's metadata for cases with empty results.
Like others suggested, you can cache the results and you won't see this. First you can try to cache it in the Widget using something like:
QuerySnapshot cache; //**
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
initialData: cache, //**
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
cache = snapshot.data; //**
This will make your widget remember the data. However, if this does not solve your problem, you would have to save it not in this widget but somewhere else. One option is to use the Provider widget to store it in a variable that lives beyond the scope of this particular widget.
Probably not related, but it's also a good idea to move the Firestore.instance.collection('events').snapshots() to initState(), save the reference to the stream in a private field and use that it StreamBuilder. Otherwise, at every build() you may be creating a new stream. You should be ready for build() calls that happen many times per second, whatever the reason.
Using Generics
Appending to #James Cameron's answer above; I found myself in a situation where, said implementation removed my typecast from withConverter. So, the below adds the generic types back into the functions.
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
extension FirestoreDocumentExtension<T> on DocumentReference<T> {
Future<DocumentSnapshot<T>> getCacheFirst() async {
try {
var ds = await get(const GetOptions(source: Source.cache));
if (!ds.exists) return get(const GetOptions(source: Source.server));
return ds;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
extension FirestoreQueryExtension<T> on Query<T> {
Future<QuerySnapshot<T>> getCacheFirst() async {
try {
var qs = await get(const GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return get(const GetOptions(source: Source.server));
return qs;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
use_case.dart
The implementation below would not compile with James example, as DocumentSnapshot<Object?> is not a subset of DocumentSnapshot<UserModel>. So, by adding the generic parameters back in, we can ensure that this extension maintains any type casts.
Future<DocumentSnapshot<UserModel>> userInfo() async {
return await FirebaseFirestore.instance
.doc("${path_to_user_model_doc}")
.withConverter<UserModel>(
fromFirestore: (snapshot, _) => UserModel.fromJson(snapshot.data()!),
toFirestore: (userModel, _) => userModel.toJson(),
)
.getCacheFirst();
}
pubspec.yaml
environment:
sdk: ">=2.17.1 <3.0.2"
dependencies:
cloud_firestore: ^3.1.17

Flutter/Firestore - Streambuilder and Firestore with realtime listener

I'm trying to set up a Firestore stream inside a stream builder like how they showed in their doc but when I update a doc or delete one it doesn't get reflected on the screen. The logs gets updated but not the actual screen.
Code:
// Chats API file
Stream<List<Message>> messagesStream({String chatID, String orderBy, bool descending, List<dynamic> startAfter, int limit}) {
return this
.collection
.document(chatID)
.collection(Config.messages)
.orderBy(orderBy, descending: descending)
.startAfter(startAfter)
.limit(limit)
.snapshots()
.map((event) => event.documents.map((documentSnapshot) => Message().model(id: documentSnapshot.documentID, map: documentSnapshot.data)).toList().reversed.toList());
}
// Chat file
Widget _streamBuilderWidget;
#override
void initState() {
super.initState();
this._streamBuilder();
if (Platform.isIOS) {
this._scrollController.addListener(() {
if (this._scrollController.position.pixels <= -135.0) {
this._checkForOldMessages();
}
});
}
}
#override
Widget build(BuildContext context) {
…
Expanded(child: this._streamBuilderWidget),
…
}
Widget _streamBuilder() {
return this._streamBuilderWidget = StreamBuilder<List<Message>>(
initialData: this._messages,
stream: APIs().chats.messagesStream(chatID: widget.chat.chatID, orderBy: 'createdAt', descending: true, startAfter: this._startAfter, limit: this._messagesLimit),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
{
return PAIndicator();
}
default:
{
if (snapshot.data.length < this._messagesLimit) {
this._hasMoreMessages = false;
} else {
this._hasMoreMessages = true;
}
snapshot.data.forEach((m) {
print(m.message); // Prints 10 values even when 1 file is changed
});
this._messages.insertAll(0, snapshot.data);
return Platform.isIOS
? MessagesList(mKey: this._listKey, scrollController: this._scrollController, messages: this._messages, aUser: widget.aUser)
: RefreshIndicator(
child: MessagesList(mKey: this._listKey, scrollController: this._scrollController, messages: this._messages, aUser: widget.aUser),
onRefresh: () async {
await Future.delayed(Duration(seconds: 1));
this._checkForOldMessages();
return null;
});
}
}
});
}
Also, when I send a message it doesn't get added to the list, I have to go back and enter the chatroom again to see the latest message.
How can I have it so that when I open the chat room I can see the new messages coming in while just showing the most recent messages to reduce costs, I don't want to show all 10000 messages. Just the most recent ones and the ones that are being added to it while users are in the chatroom.
Note: I have it like this because for somereason whenever I'm typing it keeps reloading and when I scroll the array just multiplies itself and what not. This is the only way I have add older messages to the array as I scroll that I know of.
The new messages do not show until I re-enter the chatroom.
With regards to how to only show the most recent messages, you should be implementing paginated queries and lazy load the older messages after a certain scroll point.
https://firebase.google.com/docs/firestore/query-data/query-cursors

Resources