chewie
chewie copied to clipboard
Play video in background
Thank's for this library, but i have some issues with :
- Cannot play video on background;
- Cannot play video when mobile phone screen is not lit or locked !
Thank's ^^
Could you discuss the use-case a bit more? Would you like to use Chewie for audio?
Can't speak for OP, but I have a use case to play videos with chewie, but have the option to lock the screen or move to another app while the video continues to play, either briefly or for longer periods.
I.e. play a video podcast while driving, but lock the screen so that the video is not on screen.
i agree @alexelisenko
@alexelisenko how can I just play audio when screen locked
@brianegan , i would like to play video and audio files in the background :)
Thanks, all! Seems like this is a pretty common request.
Question for you all: I don't actually work on an app that has this requirement and I'm quite constrained on time. Would anyone who needs this feature be willing to contribute the time to improve Chewie and make this possible for the community?
Happy to offer any guidance and review PRs.
Do we have to create an isolated thread to make player run even if screen is locked and/or play video in background ?
I haven't explored this problem space at all, or if the video_player plugin even supports it. It'd require some investigation into how this normally works on the Native Side, then ensure that functionality exists in video_player, THEN add it to Chewie (since Chewie is just a UI for the video_player package).
I am currently attempting to use the package 'audio_service' to keep the chewie video playing in background with a notification. Will report if/when I'm successful.
That package currently only supports Android, but if it works, I will add the iOS implementation, since I'm confident it will work for iOS.
I am currently attempting to use the package 'audio_service' to keep the chewie video playing in background with a notification. Will report if/when I'm successful.
That package currently only supports Android, but if it works, I will add the iOS implementation, since I'm confident it will work for iOS.
Waiting for you 👍
we are waiting for u broo.... hahah
waiting for you!
Update: Due to time restrictions and unforeseen complexity in disabling the video texture and leaving only the audio track playing on Android, I have resorted to using chewie and the video player in the foreground with audio muted, and using audio_service playing in the background for only the audio, and keeping the two in sync (very hacky, but works for now).
More work needs to be done to have a background video player on android, and I personally do not have enough time to take that on at the moment.
Will post here if I eventually do get to it. Thanks.
Any updates on whether this feature was ever added?
so I can't play video on iOS WebViews with this plugin?
Any updates?
any updates?
I solved the background playback issue with chewie and implemented notification with player controls using audio_service with chewie. This is my code
NOTE1: Please consider that my app is a music video player with my own ui logic so do not focus on the ui code but on the main, runApp and MtPlayerHandler class. The UI code is only to show how i use the MtPlayerHandler class.
NOTE2: My app is under development so the code is not clean and not optimized.
The main:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final mtPlayerHandler = await AudioService.init(
builder: () => MtPlayerHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'my_player_appl',
androidNotificationChannelName: 'MyPlayerApp',
androidNotificationOngoing: true));
}
// resto of code
I use Provider for DI and Bloc for state management so my runApp is:
runApp(MultiProvider(
providers: [
/// Providers
Provider<MtPlayerHandler>(create: (context) => mtPlayerHandler),
],
child: MultiProvider(
providers: [
/// Mappers
...
],
child: MultiRepositoryProvider(
providers: [
/// Repositories
...
],
child: MultiBlocProvider(providers: [
///Blocs
BlocProvider<MyPlayerCubit>(
create: (context) => MyPlayerCubit(
musicRepository: context.read<MusicRepository>(),
mtPlayerHandler: context.read<MtPlayerHandler>(),
)),
...
], child: const MyApp()),
),
),
));
this is the MtPlayerHandler class:
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:chewie/chewie.dart';
class MtPlayerHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
late VideoPlayerController videoPlayerController;
late ChewieController chewieController;
@override
Future<void> play() async {
await chewieController.videoPlayerController.play();
}
@override
Future<void> pause() async {
await chewieController.videoPlayerController.pause();
}
@override
Future<void> stop() async {
await chewieController.videoPlayerController.pause();
await chewieController.videoPlayerController.seekTo(Duration.zero);
}
@override
Future<void> seek(Duration position) async {
await chewieController.videoPlayerController.seekTo(position);
}
Future<void> startPlaying(MyVideo video) async {
// inizializza il video player controller da passare a chewie
videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(video.streamUrl!),
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true));
await videoPlayerController.initialize();
// inizializza il chewie controller per la riproduzione del video
chewieController = ChewieController(
videoPlayerController: videoPlayerController,
autoPlay: true,
);
// inizializza la coda di riproduzione
final item = MediaItem(
id: video.id!,
title: video.title!,
album: video.channelTitle!,
artUri: Uri.parse(video.thumbnailUrl!),
duration: Duration(milliseconds: video.duration!));
mediaItem.add(item);
// propaga lo stato del player ad audio_service e a tutti i listeners
chewieController.videoPlayerController.addListener(broadcastState);
}
// prepara lo stato del player per la riproduzione
void broadcastState() {
bool isPlaying() => chewieController.videoPlayerController.value.isPlaying;
AudioProcessingState audioProcessingState() {
if (chewieController.videoPlayerController.value.isInitialized) {
return AudioProcessingState.ready;
}
return AudioProcessingState.idle;
}
playbackState.add(playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
if (playbackState.value.playing)
MediaControl.pause
else
MediaControl.play,
MediaControl.skipToNext,
MediaControl.stop,
],
systemActions: const {
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 2],
processingState: audioProcessingState(),
playing: isPlaying(),
updatePosition: chewieController.videoPlayerController.value.position,
speed: chewieController.videoPlayerController.value.playbackSpeed,
));
}
}
The cubit with state:
import 'package:bloc/bloc.dart';
import 'package:chewie/chewie.dart';
import 'package:equatable/equatable.dart';
// other imports
part 'mini_player_state.dart';
class MiniPlayerCubit extends Cubit<MiniPlayerState> {
final MusicRepository musicRepository;
final MtPlayerHandler mtPlayerHandler;
MiniPlayerCubit(
{required this.musicRepository, required this.mtPlayerHandler})
: super(const MiniPlayerState.hidden());
Future<void> showMiniPlayer(String videoId) async {
emit(const MiniPlayerState.loading());
final streamUrl = await musicRepository.getStreamUrl(videoId);
final response = await musicRepository.getVideos(videoId: videoId);
final videoWithStreamUrl =
response.resources.first.copyWith(streamUrl: streamUrl);
await initPlayer(videoWithStreamUrl);
emit(MiniPlayerState.shown(streamUrl, videoWithStreamUrl,
mtPlayerHandler.chewieController, mtPlayerHandler));
}
void hideMiniPlayer() {
mtPlayerHandler.chewieController.dispose();
mtPlayerHandler.videoPlayerController.dispose();
mtPlayerHandler.videoPlayerController
.removeListener(mtPlayerHandler.broadcastState);
emit(const MiniPlayerState.hidden());
}
void pauseMiniPlayer() {
mtPlayerHandler.chewieController.pause();
}
void playMiniPlayer() {
mtPlayerHandler.chewieController.play();
}
Future<void> initPlayer(ResourceMT video) async {
await mtPlayerHandler.startPlaying(video);
}
}
the state:
part of 'mini_player_cubit.dart';
enum MiniPlayerStatus { hidden, shown, loading }
class MiniPlayerState extends Equatable {
const MiniPlayerState._(
{required this.status,
this.streamUrl,
this.video,
this.chewieController,
this.mtPlayerHandler});
final MiniPlayerStatus status;
final String? streamUrl;
final MyVideo? video;
final ChewieController? chewieController;
final MtPlayerHandler? mtPlayerHandler;
const MiniPlayerState.loading() : this._(status: MiniPlayerStatus.loading);
const MiniPlayerState.hidden() : this._(status: MiniPlayerStatus.hidden);
const MiniPlayerState.shown(String streamUrl, MyVideo video,
ChewieController chewieController, MtPlayerHandler mtPlayerHandler)
: this._(
status: MiniPlayerStatus.shown,
streamUrl: streamUrl,
video: video,
chewieController: chewieController,
mtPlayerHandler: mtPlayerHandler);
@override
List<Object?> get props => [status, streamUrl];
}
Into the main widget:
//...
BlocBuilder<MiniPlayerCubit, MiniPlayerState>(
builder: (context, state) {
switch (state.status) {
case MiniPlayerStatus.loading:
return const Center(
child: CircularProgressIndicator(),
);
case MiniPlayerStatus.shown:
return MiniPlayer(
video: state.video,
chewieController: state.chewieController!,
);
default:
return const SizedBox.shrink();
}
}),
//...
the MiniPlayer widget:
//...
class MiniPlayer extends StatelessWidget {
const MiniPlayer(
{super.key, required this.video, required this.chewieController});
final MyVideo? video;
final ChewieController chewieController;
@override
Widget build(BuildContext context) {
final MiniPlayerCubit miniPlayerCubit = context.read<MiniPlayerCubit>();
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: GestureDetector(
child: Row(
children: [
/// SzedBox con width 0 per far partire il video
/// senza che si veda il video in modalità mini player
SizedBox(
width: 0,
child: Chewie(
controller: chewieController,
)),
// Thumbnail
video?.thumbnailUrl != null
? Expanded(
child: Image.network(
video!.thumbnailUrl!,
),
)
:
//TODO: adeguare placeholder
const Expanded(
child: SizedBox(
child: FlutterLogo(),
),
),
const SizedBox(
width: 8,
),
// Title
Expanded(
flex: 2,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Flexible(
child: Text(
video?.title ?? '',
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
const SizedBox(
height: 2,
),
Row(
children: [
Flexible(
child: Text(
video?.channelTitle ?? '',
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
],
),
),
],
),
onTap: () {
showVideoPlayerBottomSheet(context);
},
)),
const SizedBox(
width: 8,
),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
//Play/pause button
StreamBuilder(
stream: miniPlayerCubit.mtPlayerHandler.playbackState
.map((playbackState) => playbackState.playing)
.distinct(),
builder: (context, snapshot) {
final isPlaying = snapshot.data ?? false;
return IconButton(
onPressed: () {
if (isPlaying) {
miniPlayerCubit.pauseMiniPlayer();
} else {
miniPlayerCubit.playMiniPlayer();
}
},
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow));
}),
//Close button
IconButton(
onPressed: () {
miniPlayerCubit.hideMiniPlayer();
},
icon: const Icon(Icons.stop)),
// button
IconButton(
onPressed: () {
showVideoPlayerBottomSheet(context);
},
icon: const Icon(Icons.expand_less)),
]),
],
),
);
}
void showVideoPlayerBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (context) => VideoPlayerBottomSheet(
video: video,
chewieController: chewieController,
));
}
}
and last the VideoPlayerBottomSheet widget:
//...imports
class VideoPlayerBottomSheet extends StatelessWidget {
const VideoPlayerBottomSheet(
{super.key, required this.video, required this.chewieController});
final MyVideo? video;
final ChewieController chewieController;
@override
Widget build(BuildContext context) {
return BottomSheet(
enableDrag: false,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topRight: Radius.circular(16))),
onClosing: () {},
builder: (_) => Scaffold(
appBar: AppBar(
title: Text(video?.title ?? ''),
),
body: Column(
children: [
AspectRatio(
aspectRatio:
chewieController.videoPlayerController.value.aspectRatio,
child: Chewie(controller: chewieController)),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(video?.description ?? ''),
),
),
),
],
),
),
);
}
}
And Now i have the notification with player controls and the mini player with player controls. I hope this can help someone.
gmstyle any updates?
gmstyle any updates?
No updates because it's solved for me with my previous post https://github.com/fluttercommunity/chewie/issues/98#issuecomment-1783746916