In my Flutter project for Windows, I want to be able to read locally at least 4 videos simultaneously (up to 1920 x 1080, up to 60 fps).
The videos can be of different formats (mp4, wmv, ...), different sizes and different framerates.
Using dart_vlc, I created a test project with 4 video players:
void main()
{
DartVLC.initialize();
runApp(const DartVLCExample());
}
class DartVLCExample extends StatelessWidget
{
const DartVLCExample({Key? key}) : super(key: key);
#override
Widget build(BuildContext context)
{
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('package:dart_vlc'),
centerTitle: true,
),
body: const PrimaryScreen(),
),
);
}
}
class PrimaryScreen extends StatelessWidget
{
const PrimaryScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context)
{
return Row(
children: const [
Expanded(child: VideoPlayer(index: 1)),
Expanded(child: VideoPlayer(index: 2)),
Expanded(child: VideoPlayer(index: 3)),
Expanded(child: VideoPlayer(index: 4)),
],
);
}
}
class VideoPlayer extends StatefulWidget
{
final int index;
const VideoPlayer({
super.key,
required this.index,
});
#override
VideoPlayerState createState() => VideoPlayerState();
}
class VideoPlayerState extends State<VideoPlayer>
{
late Player player;
MediaType mediaType = MediaType.file;
CurrentState current = CurrentState();
PositionState position = PositionState();
PlaybackState playback = PlaybackState();
GeneralState general = GeneralState();
VideoDimensions videoDimensions = const VideoDimensions(0, 0);
List<Media> medias = <Media>[];
List<Device> devices = <Device>[];
TextEditingController controller = TextEditingController();
TextEditingController metasController = TextEditingController();
double bufferingProgress = 0.0;
Media? metadataCurrentMedia;
#override
void initState()
{
super.initState();
if (mounted) {
player = Player(
id: widget.index,
videoDimensions: const VideoDimensions(640, 360),
);
player.currentStream.listen((value) {
setState(() => current = value);
});
player.positionStream.listen((value) {
setState(() => position = value);
});
player.playbackStream.listen((value) {
setState(() => playback = value);
});
player.generalStream.listen((value) {
setState(() => general = value);
});
player.videoDimensionsStream.listen((value) {
setState(() => videoDimensions = value);
});
player.bufferingProgressStream.listen(
(value) {
setState(() => bufferingProgress = value);
},
);
player.errorStream.listen((event) {
debugPrint('libVLC error.');
});
devices = Devices.all;
Equalizer equalizer = Equalizer.createMode(EqualizerMode.live);
equalizer.setPreAmp(10.0);
equalizer.setBandAmp(31.25, 10.0);
player.setEqualizer(equalizer);
medias.add(
Media.file(File("C:\\test${widget.index}.mp4"),
),
);
}
}
#override
Widget build(BuildContext context)
{
return ListView(
children: [
ClipRRect(
child: Video(
player: player,
width: 640,
height: 650,
volumeThumbColor: Colors.blue,
volumeActiveColor: Colors.blue,
showControls: true,
),
),
ElevatedButton(
onPressed: () => setState(() {
player.open(Playlist(medias: medias));
},
),
child: const Text('PLAY', style: TextStyle(fontSize: 16)),
),
],
);
}
}
But unfortunately, if I play 4 videos (1920 x 1080, 60 fps) simultaneously, the videos are jerky.
I tried to read the same videos with the official VLC player, and it works fine, so it's apparently not a problem with my machine, but rather an optimization issue. And since I'm a beginner when it comes to video players, I don't really know where to start.
So how can I optimize that, using dart_vlc or something else?
Thanks.
Related
I have several audio files to be played, so I used ListView to represent every audio file as an item of ListView, each one with its own controllers (play/pause button and duration slider). The code is as follows (I have used one audio file for all of the items for simplicity sake):
import 'package:audioplayers/audioplayers.dart';
class AudioTestScreen extends StatelessWidget {
const AudioTestScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Songs")),
body: ListView.builder(
itemCount: 10,
itemBuilder: (ctx, index) => const AudioItem(),
),
);
}
}
class AudioItem extends StatefulWidget {
const AudioItem({Key? key}) : super(key: key);
#override
State<AudioItem> createState() => _AudioItemState();
}
class _AudioItemState extends State<AudioItem> {
final audioPlayer = AudioPlayer();
bool isPlaying = false;
Duration duration = Duration.zero; // For total duration
Duration position = Duration.zero; // For the current position
#override
void initState() {
super.initState();
setAudioPlayer();
audioPlayer.onDurationChanged.listen((newDuration) {
setState(() {
duration = newDuration;
});
});
audioPlayer.onPositionChanged.listen((newPosition) {
if (mounted) {
setState(() {
position = newPosition;
});
}
});
audioPlayer.onPlayerStateChanged.listen((state) {
if (mounted) {
setState(() {
isPlaying = state == PlayerState.playing;
});
}
});
}
Future<void> setAudioPlayer() async {
final player = AudioCache(prefix: "assets/audios/");
final url = await player.load("song.mp3");
audioPlayer.setSourceUrl(url.path);
audioPlayer.setReleaseMode(ReleaseMode.stop);
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFFF4F2FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(width: 1, color: Colors.grey)
),
child: Column(
children: [
Slider(
value: position.inMilliseconds.toDouble(),
max: duration.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() {
position = Duration(milliseconds: value.toInt());
});
audioPlayer.seek(position);
},
),
GestureDetector(
onTap: () async {
isPlaying
? await audioPlayer.pause()
: await audioPlayer.resume();
},
child: CircleAvatar(
child: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
),
)
],
),
);
}
}
And here is how it looks like:
Now when I play a music file, and later tap on another item to play it, both of them plays at the same time, but I want the previous one to pause and only the current one to play.
How can I achieve this behavior? Thanks in advance.
create the audio player in the parent class and pass it to the children. Then before you play stop the player and then play it with new url
widget.player.stop()
Use this to stop the player
EDIT
class AudioItem extends StatefulWidget {
final AudioPlayer audioPlayer;
final int currentIndex;
final int index;
final VoidCallback setIndex;
const AudioItem({Key? key, required this.audioPlayer, required required this.currentIndex, required this.index, required this.setIndex}) : super(key: key);
Add these 3 variables to the Audio item. When you add these Widgets in the tree pass the values
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Songs")),
body: ListView.builder(
itemCount: 10,
itemBuilder: (ctx, index) => const AudioItem(
audioPlayer: audioPlayer,
currentIndex:currentIndex, <--this is the variable in which we know which item is playing.
index: index,
setIndex: (){
currentIndex = index;
setState((){});
}
),
),
);
}
Now when the play button is clicked call this setIndex method that will update the parent.
i need to make an audio listening application and in that need to implement a seeker. Now i can listen to audio and also able to pause it but when i seeks the audio.It starts again.
I used Audioplayers(https://pub.dev/packages/audioplayers) package version : ^0.20.1 for that.
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
class PlayAudio extends StatefulWidget {
const PlayAudio({Key? key}) : super(key: key);
#override
State<PlayAudio> createState() => _PlayAudioState();
}
class _PlayAudioState extends State<PlayAudio> {
var value=0.0;
bool isPlaying=false;
Duration duration=const Duration();
Duration position=const Duration();
AudioPlayer audioPlayer=AudioPlayer();
// AudioPlayer player = AudioPlayer();
#override
void initState() {
// TODO: implement initState
super.initState();
audioPlayer.onPlayerStateChanged.listen((event) {
setState((){
isPlaying=event==PlayerState.PLAYING;
});
});
///listen to audio duration
audioPlayer.onDurationChanged.listen((event) {
setState((){
duration=event;
});
});
audioPlayer.onAudioPositionChanged.listen((event) {
setState((){
position=event;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: ()async{
if(isPlaying){
await audioPlayer.pause();
}else if(!isPlaying){
String url="https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3";
await audioPlayer.play(url);
}
},
child: Container(
color: Colors.red,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("play Audio"),
),
),
),
Slider.adaptive(
min: 0.0,
value: position.inSeconds.toDouble(),
max: 120,
onChanged: (value1)async{
Duration newDuration = Duration(seconds: value1.toInt());
await audioPlayer.seek(newDuration);
}),
Text(formatTime(position)),
Text(formatTime(duration-position)),
],
),
),
);
}
String formatTime(Duration duration){
String twoDigits(int n)=>n.toString().padLeft(2,"0");
final hours=twoDigits(duration.inHours);
final minutes=twoDigits(duration.inMinutes.remainder(60));
final seconds=twoDigits(duration.inSeconds.remainder(60));
return [
if(duration.inHours>0) hours,minutes,seconds
].join(":");
}
}
when i checked it on IOS simulator it working fine but in android real device it showing the problem.
I am a beginner to rive and flutter. I am building a favorite items page in flutter. If there are not anything added to favorites I need to show a riveAnimation on screen. I already implemented almost everything to show the animation on screen. But I need to toggle a jumping animation when user tap on the animation which is really cool. for now I have the animation on 'Idle' mode
You may want to refer to the rive file => Go to Rive. And I renamed Rive stateMachine name to Bird. Everything else is the same.
summary => I want bird to jump when user tap on him :)
The code and the image may be little bit bigger. Sorry about that
class Favourites extends StatefulWidget {
Favourites({Key? key}) : super(key: key);
#override
State<Favourites> createState() => _FavouritesState();
}
class _FavouritesState extends State<Favourites> {
String animation = 'idle';
SMIInput<String>? _birdInput;
Artboard? _birdArtboard;
void jump() {
setState(() {
_birdInput?.value = 'Pressed';
});
}
#override
void initState() {
super.initState();
rootBundle.load('assets/rive/bird.riv').then(
(data) {
final file = RiveFile.import(data);
final artboard = file.mainArtboard;
var controller = StateMachineController.fromArtboard(
artboard,
'Bird',
);
if (controller != null) {
artboard.addController(controller);
_birdInput = controller.findInput('Pressed');
}
setState(() => _birdArtboard = artboard);
},
);
}
#override
Widget build(BuildContext context) {
final favourite = Provider.of<Favourite>(context);
return Scaffold(
backgroundColor: Colors.grey[300],
appBar: const CustomAppBar(title: 'Favourites'),
body: favourite.items.isEmpty
? Center(
child: Column(
children: [
SizedBox(
width: 300,
height: 500,
child: _birdArtboard == null
? const SizedBox()
: Center(
child: GestureDetector(
onTap: () {},
child: Rive(artboard: _birdArtboard!),
),
),
),
NeumorphicButton(),
],
),
)
: CustomGrid(),
);
}
}
If you open/run rive file on rive site, you can find that it is using Trigger variable for jumping and it is using State Machine 1 state machine.
Next thing comes about declaring variable. You need to use SMITrigger data type for this and use StateMachineController to control the animation.
Use .findSMI(..) instead of .findInput() for SMITrigger.
To start animation on trigger, use
trigger?.fire();
I will encourage you to take a look on editor and check input variable type while performing rive animation.
So the full widget that will provide animation is
class Favourites extends StatefulWidget {
const Favourites({Key? key}) : super(key: key);
#override
State<Favourites> createState() => _FavouritesState();
}
class _FavouritesState extends State<Favourites> {
String animation = 'idle';
Artboard? _birdArtboard;
SMITrigger? trigger;
StateMachineController? stateMachineController;
#override
void initState() {
super.initState();
rootBundle.load('assets/rive/bird.riv').then(
(data) {
final file = RiveFile.import(data);
final artboard = file.mainArtboard;
stateMachineController =
StateMachineController.fromArtboard(artboard, "State Machine 1");
if (stateMachineController != null) {
artboard.addController(stateMachineController!);
trigger = stateMachineController!.findSMI('Pressed');
stateMachineController!.inputs.forEach((e) {
debugPrint(e.runtimeType.toString());
debugPrint("name${e.name}End");
});
trigger = stateMachineController!.inputs.first as SMITrigger;
}
setState(() => _birdArtboard = artboard);
},
);
}
void jump() {
trigger?.fire();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[300],
body: Center(
child: Column(
children: [
SizedBox(
width: 300,
height: 400,
child: _birdArtboard == null
? const SizedBox()
: Center(
child: GestureDetector(
onTap: () {
jump();
},
child: Rive(artboard: _birdArtboard!),
),
),
),
],
),
));
}
}
I've created a .riv file with 3 state animations: start, processing, end, which are in "State machine". Rive team recently announced a new feature with dinamically changing animations, it's "State machine". Not sure, how to use it in flutter project, i.e how to dynamically change value of animation. If somebody needs some code, no problem, I could provide. Moreover, link to rive's "state machine" https://www.youtube.com/watch?v=0ihqZANziCk. I didn't find any examples related to this new feature. Please help! Thanks.
The other answer is outdated.
class SimpleStateMachine extends StatefulWidget {
const SimpleStateMachine({Key? key}) : super(key: key);
#override
_SimpleStateMachineState createState() => _SimpleStateMachineState();
}
class _SimpleStateMachineState extends State<SimpleStateMachine> {
SMITrigger? _bump;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(artboard, 'bumpy');
artboard.addController(controller!);
_bump = controller.findInput<bool>('bump') as SMITrigger;
}
void _hitBump() => _bump?.fire();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: Center(
child: GestureDetector(
child: RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
fit: BoxFit.cover,
onInit: _onRiveInit,
),
onTap: _hitBump,
),
),
);
}
}
See the RIVE guide:
https://help.rive.app/runtimes/state-machines
There are examples on rives pub package site. Here is one for state machine.
example_state_machine.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
/// An example showing how to drive two boolean state machine inputs.
class ExampleStateMachine extends StatefulWidget {
const ExampleStateMachine({Key? key}) : super(key: key);
#override
_ExampleStateMachineState createState() => _ExampleStateMachineState();
}
class _ExampleStateMachineState extends State<ExampleStateMachine> {
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard? _riveArtboard;
StateMachineController? _controller;
SMIInput<bool>? _hoverInput;
SMIInput<bool>? _pressInput;
#override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('assets/rocket.riv').then(
(data) async {
// Load the RiveFile from the binary data.
final file = RiveFile.import(data);
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
var controller =
StateMachineController.fromArtboard(artboard, 'Button');
if (controller != null) {
artboard.addController(controller);
_hoverInput = controller.findInput('Hover');
_pressInput = controller.findInput('Press');
}
setState(() => _riveArtboard = artboard);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: const Text('Button State Machine'),
),
body: Center(
child: _riveArtboard == null
? const SizedBox()
: MouseRegion(
onEnter: (_) => _hoverInput?.value = true,
onExit: (_) => _hoverInput?.value = false,
child: GestureDetector(
onTapDown: (_) => _pressInput?.value = true,
onTapCancel: () => _pressInput?.value = false,
onTapUp: (_) => _pressInput?.value = false,
child: SizedBox(
width: 250,
height: 250,
child: Rive(
artboard: _riveArtboard!,
),
),
),
),
),
);
}
}
I am trying to develop an app that presents videos to the user. I am using VideoPlayerController for loading the videos, and ChewieController for the UI.
It works great, but when the user closes the app, the video stops. I would like the video to keep playing its audio even when closing the app/locking the device.
I couldn't find anything about it on the VideoPlayerController and in the ChewieController documentations.
Is this functionality possible in Flutter and Dart?
Thank you!
Unfortunately Flutter's video_player package doesn't support background video or audio playing. But you can use flutter_playout which wraps ExoPlayer on Android and AVPlayer framework on iOS with the ability to playback video in background or even lock screen. You can find out more about it here. Below is an example code provided by library's GitHub repo which plays a video and it keeps playing in background
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_playout/multiaudio/HLSManifestLanguage.dart';
import 'package:flutter_playout/multiaudio/MultiAudioSupport.dart';
import 'package:flutter_playout/player_observer.dart';
import 'package:flutter_playout/player_state.dart';
import 'package:flutter_playout/video.dart';
import 'package:flutter_playout_example/hls/getManifestLanguages.dart';
class VideoPlayout extends StatefulWidget {
final PlayerState desiredState;
final bool showPlayerControls;
const VideoPlayout({Key key, this.desiredState, this.showPlayerControls})
: super(key: key);
#override
_VideoPlayoutState createState() => _VideoPlayoutState();
}
class _VideoPlayoutState extends State<VideoPlayout>
with PlayerObserver, MultiAudioSupport {
final String _url = null;
List<HLSManifestLanguage> _hlsLanguages = List<HLSManifestLanguage>();
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, _getHLSManifestLanguages);
}
Future<void> _getHLSManifestLanguages() async {
if (!Platform.isIOS && _url != null && _url.isNotEmpty) {
_hlsLanguages = await getManifestLanguages(_url);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
/* player */
AspectRatio(
aspectRatio: 16 / 9,
child: Video(
autoPlay: true,
showControls: widget.showPlayerControls,
title: "MTA International",
subtitle: "Reaching The Corners Of The Earth",
preferredAudioLanguage: "eng",
isLiveStream: true,
position: 0,
url: _url,
onViewCreated: _onViewCreated,
desiredState: widget.desiredState,
),
),
/* multi language menu */
_hlsLanguages.length < 2 && !Platform.isIOS
? Container()
: Container(
child: Row(
children: _hlsLanguages
.map((e) => MaterialButton(
child: Text(
e.name,
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white),
),
onPressed: () {
setPreferredAudioLanguage(e.code);
},
))
.toList(),
),
),
],
),
);
}
void _onViewCreated(int viewId) {
listenForVideoPlayerEvents(viewId);
enableMultiAudioSupport(viewId);
}
#override
void onPlay() {
// TODO: implement onPlay
super.onPlay();
}
#override
void onPause() {
// TODO: implement onPause
super.onPause();
}
#override
void onComplete() {
// TODO: implement onComplete
super.onComplete();
}
#override
void onTime(int position) {
// TODO: implement onTime
super.onTime(position);
}
#override
void onSeek(int position, double offset) {
// TODO: implement onSeek
super.onSeek(position, offset);
}
#override
void onDuration(int duration) {
// TODO: implement onDuration
super.onDuration(duration);
}
#override
void onError(String error) {
// TODO: implement onError
super.onError(error);
}
}
As the video_player package now has the allowBackgroundPlayback option, I created this simple example showing how to integrate video_player and audio service.
example_video_player.dart
// This example demonstrates a simple video_player integration.
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
// You might want to provide this using dependency injection rather than a
// global variable.
late AudioPlayerHandler _audioHandler;
Future<void> main() async {
_audioHandler = await AudioService.init(
builder: () => AudioPlayerHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.ryanheise.myapp.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: true,
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Audio Service Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
late VideoPlayerController _controller;
#override
void initState() {
super.initState();
_controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true))
..initialize().then((_) {
_audioHandler.setVideoFunctions(_controller.play, _controller.pause, _controller.seekTo, () {
_controller.seekTo(Duration.zero);
_controller.pause();
});
// 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...
_audioHandler.initializeStreamController(_controller);
_audioHandler.playbackState.addStream(_audioHandler.streamController.stream);
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}
#override
void dispose() {
// Close the stream
_audioHandler.streamController.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Audio Service Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Container(),
),
// Play/pause/stop buttons.
StreamBuilder<bool>(
stream: _audioHandler.playbackState.map((state) => state.playing).distinct(),
builder: (context, snapshot) {
final playing = snapshot.data ?? false;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_button(Icons.fast_rewind, _audioHandler.rewind),
if (playing) _button(Icons.pause, _audioHandler.pause) else _button(Icons.play_arrow, _audioHandler.play),
_button(Icons.stop, _audioHandler.stop),
_button(Icons.fast_forward, _audioHandler.fastForward),
],
);
},
),
// Display the processing state.
StreamBuilder<AudioProcessingState>(
stream: _audioHandler.playbackState.map((state) => state.processingState).distinct(),
builder: (context, snapshot) {
final processingState = snapshot.data ?? AudioProcessingState.idle;
return Text("Processing state: ${(processingState)}");
},
),
],
),
),
);
}
IconButton _button(IconData iconData, VoidCallback onPressed) => IconButton(
icon: Icon(iconData),
iconSize: 64.0,
onPressed: onPressed,
);
}
class MediaState {
final MediaItem? mediaItem;
final Duration position;
MediaState(this.mediaItem, this.position);
}
/// An [AudioHandler] for playing a single item.
class AudioPlayerHandler extends BaseAudioHandler with SeekHandler {
late StreamController<PlaybackState> streamController;
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'),
);
Function? _videoPlay;
Function? _videoPause;
Function? _videoSeek;
Function? _videoStop;
void setVideoFunctions(Function play, Function pause, Function seek, Function stop) {
_videoPlay = play;
_videoPause = pause;
_videoSeek = seek;
_videoStop = stop;
mediaItem.add(_item);
}
/// Initialise our audio handler.
AudioPlayerHandler();
// 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() async => _videoPlay!();
#override
Future<void> pause() async => _videoPause!();
#override
Future<void> seek(Duration position) async => _videoSeek!(position);
#override
Future<void> stop() async => _videoStop!();
void initializeStreamController(VideoPlayerController? videoPlayerController) {
bool _isPlaying() => videoPlayerController?.value.isPlaying ?? false;
AudioProcessingState _processingState() {
if (videoPlayerController == null) return AudioProcessingState.idle;
if (videoPlayerController.value.isInitialized) return AudioProcessingState.ready;
return AudioProcessingState.idle;
}
Duration _bufferedPosition() {
DurationRange? currentBufferedRange = videoPlayerController?.value.buffered.firstWhere((durationRange) {
Duration position = videoPlayerController.value.position;
bool isCurrentBufferedRange = durationRange.start < position && durationRange.end > position;
return isCurrentBufferedRange;
});
if (currentBufferedRange == null) return Duration.zero;
return currentBufferedRange.end;
}
void _addVideoEvent() {
streamController.add(PlaybackState(
controls: [
MediaControl.rewind,
if (_isPlaying()) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.fastForward,
],
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
androidCompactActionIndices: const [0, 1, 3],
processingState: _processingState(),
playing: _isPlaying(),
updatePosition: videoPlayerController?.value.position ?? Duration.zero,
bufferedPosition: _bufferedPosition(),
speed: videoPlayerController?.value.playbackSpeed ?? 1.0,
));
}
void startStream() {
videoPlayerController?.addListener(_addVideoEvent);
}
void stopStream() {
videoPlayerController?.removeListener(_addVideoEvent);
streamController.close();
}
streamController = StreamController<PlaybackState>(onListen: startStream, onPause: stopStream, onResume: startStream, onCancel: stopStream);
}
}
I've been using the better_player package. It's quite good uses video_player and chewie and also has support for player notification and PiP.
And don't forget to enable the background audio capability on your xcode.
xcode-audio-capability