I am working on an app where I show videos in a PageView
like TikTok. It was very hard with the video_player
package to make it work, but I was able to do it correctly. I did it so that the first time it downloads two or three videos into my temporary directory using getTemporaryDirectory()
of path_provider
package. Then when the download is complete, I play the video using VideoPlayerController.file(myDownloadedFile)
. This usually works fine, but sometimes when I first start from the first video (the videos are pre-downloaded into the temporary directory), it will take more time (a minute or so) to initialize the VideoPlayerController
with this local downloaded file. I use a black page to show while the VideoPlayerController
is initializing, which usually takes a fraction of a second, but in these rare cases it takes up to a minute or more. If I scroll to other videos, they will also show this black page indicating that they initializes the VideoPlayerController
although I use different VideoPlayerController
for each video. But when the first one finishes initializing after a long time, the others will also work fine.
In other cases, for some of the videos that are recorded by iPhone, the video_player shows a white screen. How can we make it compatible with video_player?
Anyone, please help me what is wrong with this!
The code is as follows:
video.dart
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:video_player/video_player.dart';
class Video {
Video(this.videoUrl);
String videoUrl = '';
VideoPlayerController? controller;
File? file;
bool isDownloadStarted = false;
final BehaviorSubject<bool> _downloadStreamController =
BehaviorSubject<bool>();
final BehaviorSubject<bool> _initializationStreamController =
BehaviorSubject<bool>();
Stream<bool> get downloadStream => _downloadStreamController.stream;
Stream<bool> get initializationStream =>
_initializationStreamController.stream;
Future<void> downloadVideo() async {
if (!isDownloadStarted) {
Directory temporaryDirectory = await getTemporaryDirectory();
String fullPath = '${temporaryDirectory.path}/${_getFileName(videoUrl)}';
file = File(fullPath);
isDownloadStarted = true;
if (!(await file!.exists())) {
try {
Response<dynamic> response = await Dio().get<dynamic>(
videoUrl,
onReceiveProgress: (int received, int total) {},
options: Options(
responseType: ResponseType.bytes, followRedirects: false),
);
RandomAccessFile raf = file!.openSync(mode: FileMode.write);
raf.writeFromSync(response.data);
raf.close();
} catch (e) {
isDownloadStarted = false;
return;
}
}
_downloadStreamController.sink.add(true);
}
}
Future<void> initializePlayer() async {
controller = VideoPlayerController.file(file!);
await controller!.initialize();
_initializationStreamController.sink.add(true);
play();
controller!.setLooping(true);
}
String _getFileName(String url) {
int i = url.lastIndexOf('/');
return url.substring(i + 1);
}
Future<void> dispose() async {
if (controller != null) {
await controller!.dispose();
}
}
bool get isPlaying => controller!.value.isPlaying;
Future<void> play() async {
await controller?.play();
}
Future<void> pause() async {
await controller?.pause();
}
Future<void> playPause() async {
isPlaying ? await pause() : await play();
}
}
video_provider.dart
import 'package:flutter/material.dart';
import 'video.dart';
class VideoProvider extends ChangeNotifier {
List<Video> videos = <Video>[
Video('video1 url'),
Video('video2 url'),
Video('video3 url'),
Video('video4 url'),
Video('video5 url'),
Video('video6 url'),
];
bool isPlaying = false;
int _currentVideoIndex = 0;
set currentVideoIndex(int index) {
_currentVideoIndex = index;
notifyListeners();
}
int get currentVideoIndex => _currentVideoIndex;
Future<void> init() async {
if (videos.isNotEmpty) {
await downloadVideos();
}
}
Future<void> downloadVideos() async {
await videos[_currentVideoIndex].downloadVideo();
// Download one previous video
if (_currentVideoIndex - 1 >= 0) {
videos[_currentVideoIndex - 1].downloadVideo();
}
// Download two next videos
for (int i = _currentVideoIndex + 1;
i < _currentVideoIndex + 3 && i < videos.length;
i++) {
videos[i].downloadVideo();
}
}
}
video_card.dart
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'video.dart';
class VideoCard extends StatefulWidget {
const VideoCard(this.video, {Key? key}) : super(key: key);
final Video video;
@override
_VideoCardState createState() => _VideoCardState();
}
class _VideoCardState extends State<VideoCard> {
@override
void dispose() {
widget.video.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: widget.video.downloadStream,
builder: (BuildContext ctx, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _loadingWidget();
} else if (snapshot.hasData && !snapshot.data!) {
return const Text('Download failed!');
}
return FutureBuilder<void>(
future: widget.video.initializePlayer(), // Sometimes it stucks in here for more than a minute cuasing to show the black container
builder: (BuildContext ctx, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return GestureDetector(
onTap: widget.video.playPause,
child: SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: widget.video.controller!.value.size.width,
height: widget.video.controller!.value.size.height,
child: VideoPlayer(widget.video.controller!),
),
),
),
);
}
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
);
},
);
},
);
}
Widget _loadingWidget() {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.black,
child: const Center(
child: CircularProgressIndicator(
backgroundColor: Colors.white,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blueAccent),
),
),
);
}
}
video_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'video_card.dart';
import 'video_provider.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({Key? key}) : super(key: key);
@override
_VideoScreenState createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
late VideoProvider videoProvider;
@override
Widget build(BuildContext context) {
videoProvider = Provider.of<VideoProvider>(context, listen: false);
return FutureBuilder<void>(
future: videoProvider.init(),
builder: (BuildContext ctx, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator.adaptive());
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return videoProvider.videos.isNotEmpty
? PageView.builder(
controller: PageController(
initialPage: videoProvider.currentVideoIndex),
itemCount: videoProvider.videos.length,
onPageChanged: (int index) async {
videoProvider.currentVideoIndex = index;
videoProvider.downloadVideos();
},
scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) =>
VideoCard(videoProvider.videos[index]))
: const Center(child: Text('No videos available!'));
},
);
}
}