Flutter Video Feeds (Like Instagram or Facebook)
There are many packages for the video player in pub.dev but it is very hectic to maintain the video player in list view as it throws some kind of dispose error or some kind of crashes occurred during playing videos.
I tried a lot of packages but at first, I was not able to build video feeds correctly. After I tried using native package by flutter video_player but I got error such as dispose error and sometimes only 4 out of 10 videos are playing and others crash by throwing platform Exception
A VideoPlayerController was used after being disposed.
I/flutter : Once you have called dispose() on a VideoPlayerController, it can no longer be used.
or
[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: PlatformException(VideoError, Video player had error com.google.android.exoplayer2.ExoPlaybackException: Source error, null, null)
So, to get rid of these errors, I need to dispose the video controller which is no more in the viewport, and re-initialize with the new controller when it is in the viewport. For this purpose, I used the visibility_detector package to check the visibility of a particular list of items.
Also, I need to define a global variable in order to make sure that no 2 videos are running in parallel.
CODE:
main.dart
import 'package:flutter/material.dart';
import 'package:social_media_video_feeds/utils.dart';
import 'package:social_media_video_feeds/video_player.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: VideoListView());
}
}
class VideoListView extends StatelessWidget {
const VideoListView({
Key? key,
}) : super(key: key);
@override
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Video Feeds"),
),
body: ListView.builder(
itemBuilder: (context, index) {
return VideoPlayer(
videoUrl: videos_url[index], thumbnailUrl: thumbs_Url[index]);
},
itemCount: videos_url.length,
padding: EdgeInsets.symmetric(vertical: 10.0),
),
);
}
}
utils.dart
const List<String> videos_url = [
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
];
video_player.dart
import 'dart:async';
import 'package:cached_video_player/cached_video_player.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:visibility_detector/visibility_detector.dart';
CachedVideoPlayerController? activeController;
class VideoPlayer extends StatefulWidget {
final String videoUrl;
final String thumbnailUrl;
const VideoPlayer({
Key? key,
required this.videoUrl,
required this.thumbnailUrl,
}) : super(key: key);
@override
_VideoPlayerState createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
CachedVideoPlayerController? _videoController;
final UniqueKey stickyKey = UniqueKey();
bool isControllerReady = false;
bool isPlaying = false;
Completer videoPlayerInitializedCompleter = Completer();
@override
void initState() {
super.initState();
}
@override
void dispose() async {
if (_videoController != null)
await _videoController?.dispose()?.then((_) {
isControllerReady = false;
_videoController = null;
videoPlayerInitializedCompleter = Completer(); // resets the Completer
});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
child: VisibilityDetector(
key: stickyKey,
onVisibilityChanged: (VisibilityInfo info) async {
if (info.visibleFraction > 0.70) {
if (_videoController == null) {
_videoController =
CachedVideoPlayerController.network(widget.videoUrl);
_videoController!.initialize().then((_) async {
videoPlayerInitializedCompleter.complete(true);
setState(() {
isControllerReady = true;
});
_videoController!.setLooping(true);
});
}
} else if (info.visibleFraction < 0.30) {
setState(() {
isControllerReady = false;
});
_videoController?.pause();
setState(() {
isPlaying = false;
});
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (activeController == _videoController) {
activeController = null;
}
_videoController?.dispose()?.then((_) {
setState(() {
_videoController = null;
videoPlayerInitializedCompleter =
Completer(); // resets the Completer
});
});
});
}
},
child: FutureBuilder(
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
_videoController != null &&
isControllerReady) {
// should also check that the video has not been disposed
return GestureDetector(
onTap: () async {
setState(() {
if (_videoController!.value.isPlaying) {
_videoController?.pause();
setState(() {
isPlaying = false;
});
} else {
if (activeController != null) {
setState(() {
activeController!.pause();
});
}
activeController = _videoController;
_videoController?.play();
setState(() {
isPlaying = true;
});
}
});
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
AspectRatio(
aspectRatio: 4 / 3,
child: CachedVideoPlayer(_videoController)),
!isPlaying
? Icon(
CupertinoIcons.play_arrow_solid,
color: Colors.white70,
size: 54,
)
: Container(
height: 0.0,
),
],
)); // display the video
}
return AspectRatio(
aspectRatio: 4 / 3,
child: Container(
color: Colors.black,
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
future: videoPlayerInitializedCompleter.future,
),
),
);
}
}
DEMO VIDEO:
#Happy Coding 😊
Very informative blog.
ReplyDeleteThis comment has been removed by the author.
Deleteyes vey important thing
DeleteGood One, Similar to what I want, Can you share your email id for a connect
ReplyDelete