Reputation: 123
I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player
is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyHomePage(),
));
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<VideoPlayerController> _controllers = [];
VoidCallback listener;
bool _isPlaying = false;
int _current = 0;
@override
void initState() {
super.initState();
// Add some sample videos
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
this.tick();
// Try refreshing by brute force (this isn't going too well)
new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
int delta = 99999999;
if(_controllers[_current].value != null) {
delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
}
print("Tick " + delta.toString());
if(delta < 500) {
_current += 1;
this.tick();
}
});
}
void tick() async {
print("Current: " + _current.toString());
await _controllers[_current].initialize();
await _controllers[_current].play();
print("Ready");
setState((){
_current = _current;
});
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: _controllers[_current].value.aspectRatio,
// Use the VideoPlayer widget to display the video
child: VideoPlayer(_controllers[_current]),
);
}
}
What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.
Upvotes: 12
Views: 15922
Reputation: 967
I found that better_player
consumes a significant amount of RAM in my app, especially when playing a playlist with multiple videos. Without adding the android:largeHeap="true"
flag in the AndroidManifest.xml
, I encountered Out-of-Memory (OOM) errors and app crashes during video playback. After switching to chewie
+ video_player
, I saw a dramatic reduction in RAM usage, and the need for the largeHeap
flag was eliminated.
In the Android Memory Profiler, you can see how better_player
uses considerably more RAM compared to the combination of chewie
+ video_player
. Below are the relevant screenshots:
Screenshot 1: RAM usage with better_player
while playing a playlist of two videos.
Screenshot 2: RAM usage with chewie
+ video_player
for the same videos.
This significant reduction in memory consumption eliminates the need for the largeHeap
flag, which is also recommended by Google to avoid unless absolutely necessary.
Example showing how to use the chewie + video_player combo to play a playlist of videos sequentially without delay:
import 'package:flutter/material.dart';
import 'package:chewie/chewie.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(const MaterialApp(home: VideoPlayerPlaylistDemo()));
}
class VideoPlayerPlaylistDemo extends StatefulWidget {
const VideoPlayerPlaylistDemo({super.key});
@override
State<VideoPlayerPlaylistDemo> createState() => _VideoPlayerPlaylistDemoState();
}
class _VideoPlayerPlaylistDemoState extends State<VideoPlayerPlaylistDemo> {
int _currentIndex = 0;
List<String> videoUrls = [
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
];
late VideoPlayerController _videoPlayerController;
ChewieController? _chewieController;
@override
void initState() {
super.initState();
_initializeVideoPlayer(_currentIndex);
}
Future<void> _initializeVideoPlayer(int index) async {
_videoPlayerController = VideoPlayerController.network(videoUrls[index]);
await _videoPlayerController.initialize();
_createChewieController();
_videoPlayerController.addListener(() {
// Check if the video has finished playing
if (_videoPlayerController.value.position ==
_videoPlayerController.value.duration) {
_playNextVideo();
}
});
setState(() {});
}
void _createChewieController() {
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoPlay: true,
looping: false, // No looping since we want sequential playback
);
}
void _playNextVideo() {
if (_currentIndex < videoUrls.length - 1) {
_currentIndex++;
_chewieController?.dispose();
_videoPlayerController.dispose();
_initializeVideoPlayer(_currentIndex);
} else {
// Optionally reset to the first video or show a message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Playlist finished")),
);
}
}
@override
void dispose() {
_videoPlayerController.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Chewie Playlist Example")),
body: Center(
child: _chewieController != null && _chewieController!.videoPlayerController.value.isInitialized
? Chewie(controller: _chewieController!)
: const CircularProgressIndicator(),
),
);
}
}
Upvotes: 0
Reputation: 5086
Update
There is a preCache()
method in the better_player
library, which uses the underlying native players' cache implementations. It would be an ideal solution for seamless sequential video playback. The old answer below is a "hacky" way of achieving this.
Unfortunately, the video_player
library still has no pre-caching (or even caching) feature.
Old answer
Initializing a VideoPlayerController.network()
may take some time to finish. You can initialize the controller of the next video while playing the current one. This will take more memory, but I don't think it will create huge problems if you prebuffer only one or two videos. Then, when the next or previous buttons are pressed, the video will be ready to play.
Here is my workaround. It prebuffers the previous and next videos, skips to the next video when finished, shows the current position and buffer, and pauses and plays on a long press.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
main() {
runApp(const MaterialApp(
home: VideoPlayerDemo(),
));
}
class VideoPlayerDemo extends StatefulWidget {
const VideoPlayerDemo({super.key});
@override
State<VideoPlayerDemo> createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool _lock = true;
final Map<String, VideoPlayerController> _controllers = {};
final Map<int, VoidCallback> _listeners = {};
static const _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
};
@override
void initState() {
super.initState();
if (_urls.isNotEmpty) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => _lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)]!;
}
Future<void> _initController(int index) async {
final url = Uri.parse(_urls.elementAt(index));
var controller = VideoPlayerController.networkUrl(url);
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]!);
_controller(index).pause();
_controller(index).seekTo(const Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]!);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (_lock || index == 0) {
return;
}
_lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
_lock = false;
} else {
_initController(index - 1).whenComplete(() => _lock = false);
}
}
void _nextVideo() async {
if (_lock || index == _urls.length - 1) {
return;
}
_lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
_lock = false;
} else {
_initController(index + 1).whenComplete(() => _lock = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Playing ${index + 1} of ${_urls.length}"),
),
body: Stack(
children: <Widget>[
GestureDetector(
onLongPressStart: (_) => _controller(index).pause(),
onLongPressEnd: (_) => _controller(index).play(),
child: Center(
child: AspectRatio(
aspectRatio: _controller(index).value.aspectRatio,
child: Center(child: VideoPlayer(_controller(index))),
),
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _buffer,
color: Colors.grey,
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _position,
color: Colors.greenAccent,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: _previousVideo,
child: const Icon(Icons.arrow_back),
),
const SizedBox(width: 24),
FloatingActionButton(
onPressed: _nextVideo,
child: const Icon(Icons.arrow_forward),
),
],
),
);
}
}
All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.
Upvotes: 20