How to play HTTP live stream on IOS device with Flutter APP? - ios

I'm looking for approaches to play video Live Stream in Flutter.
I tested on Chewie and Video_player plugins. It works well on Android but doesn`t on IOS devices. And unfortunately, debug console is empty...
Here is working .m3u8 file I tried to play.
Here is a simple reproducer:
import 'package:video_player/video_player.dart';
import 'package:flutter/material.dart';
void main() => runApp(VideoApp());
class VideoApp extends StatefulWidget {
#override
_VideoAppState createState() => _VideoAppState();
}
class _VideoAppState extends State<VideoApp> {
VideoPlayerController _controller;
#override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://streamvideo.luxnet.ua/news24/smil:news24.stream.smil/playlist.m3u8')
..addListener(() {
if (_controller.value.initialized) {
print(_controller.value.position);
}
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Demo',
home: Scaffold(
body: Center(
child: Stack(
children: <Widget>[
_controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Container()
],
)
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.orange,
onPressed: () {
setState(() {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
Maybe somebody has experience with playing Video Stream on IOS with Flutter? Thanks a lot! 🍺

The video_player plugin doesn't currently support the iOS simulators. Your code is likely working correctly on a physical device.

The problem is in:
if ([self duration] == 0) {
return;
}
It is in platform level FLTVideoPlayerPlugin.m . Line number 325. It seems like duration is zero for live videos. If you remove that part of code it should work.

Related

Getting an image-related exception when using a ListWheelScrollView in Flutter

I'm getting a peculiar bug when using a ListWheelScrollView to display image widgets on iOS. It is contained in one page of a PageView, and it works fine until I minimize the app. If the app is resumed after entering the background and then I switch to the page that contains the scrollview (either by switching away and switching back after resuming or by switching away before minimizing and then switching back after resuming), the visible images fail to display and the output reads as follows:
════════ Exception caught by image resource service
════════════════════════════ The method 'toDouble' was called on null.
Receiver: null Tried calling: toDouble()
════════════════════════════════════════════════════════════════════════════════
Below is a simple example that demonstrates the problem:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ListWheel Issue',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'ListWheelScrollview Bug'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _currentIndex = 0;
PageController _pageController;
List<Widget> get _tabs => [
ScrollScreen(),
Container(),
Container(),
];
#override
void initState() {
_pageController = PageController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: PageView(
controller: _pageController,
children: _tabs,
)),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.refresh), title: Text('tab1')),
BottomNavigationBarItem(icon: Icon(Icons.movie), title: Text('tab2')),
BottomNavigationBarItem(
icon: Icon(Icons.search), title: Text('tab3')),
],
currentIndex: _currentIndex,
onTap: (i) {
setState(() {
_currentIndex = i;
_pageController.animateToPage(i,
curve: Curves.easeOut, duration: Duration(milliseconds: 200));
});
},
),
);
}
}
class ScrollScreen extends StatefulWidget {
#override
_ScrollScreenState createState() => _ScrollScreenState();
}
class _ScrollScreenState extends State<ScrollScreen> {
var _exImage = AssetImage('assets/images/no_image.png');
#override
Widget build(BuildContext context) {
List<AssetImage> _images = [
_exImage,
_exImage,
_exImage,
];
return Center(
child: ListWheelScrollView.useDelegate(
itemExtent: 300,
childDelegate: ListWheelChildLoopingListDelegate(
children: _images
.map((e) => Center(
child: Image(
image: e,
)))
.toList())),
);
}
}
Any help with this would be greatly appreciated.
I found out a solution.
#override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(AssetImage('assets/icons/settings_icon.png'), context);
}
after precaching the images used in the ListWheelScrollView, the error no longer appears
Idk if flutter team will fix it in the future or not.
I figured out a bit more about the issue. It turns out null values were being passed as "minScrollExtent" and "maxScrollExtent" into the _getItemFromOffset function in the list_wheel_scroll_view.dart file, which were, in turn being passed into the _clipOffsetToScrollableRange function.
As a workaround, I ended up changing the return value of the _clipOffsetScrollableRange to the following:
return (_clipOffsetToScrollableRange(
offset,
minScrollExtent ?? -double.infinity,
maxScrollExtent ?? double.infinity) /
itemExtent)
.round();
This likely does not address the underlying issue, but has resulted in my app working properly.

Flutter video_player replaying video loaded from assets

I am trying using the video_player package to try and load, play, and then replay a video from assets. The video loads correctly and plays fine on the first run through. However when I try to replay the video using _controller.seekTo(Duration.zero) and _controller.play() it remains frozen on the last frame of the video and won't replay back.
This occurs only in iOS but not in Android where it behaves as expected. And it only occurs for videos loaded from assets. If I load a video from a url using VideoPlayerController.network it also behaves as expected.
Below is the class I modified from the video_player example.
import 'package:video_player/video_player.dart';
import 'package:flutter/material.dart';
class VideoApp extends StatefulWidget {
#override
_VideoAppState createState() => _VideoAppState();
}
class _VideoAppState extends State<VideoApp> {
VideoPlayerController _controller;
#override
void initState() {
super.initState();
_controller = VideoPlayerController.asset('assets/video/MOV_GET_OUT_BED.mp4')
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Demo',
home: Scaffold(
body: Column(
children: <Widget>[
Center(
child: _controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: Container(),
),
GestureDetector(
onTap: () async {
await _controller.seekTo(Duration.zero);
setState( () {
_controller.play();
} );
},
child: Text("Restart video")
),
]
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
To auto replay, add this to your initState() method.
_controller.setLooping(true);

Flutter video_player restart video from start on tap

I am using the flutter video_player package to play a short video file using in my application. I inspired from the flutter cookbook: Play and pause a video.
I would like to allow the user to tap on the video to restart it from beginning. So I wrapped the VideoPlayer with a GestureDetector.
I currently have the following code:
class MyVideoPlayer extends StatefulWidget {
final File videoFile;
MyVideoPlayer({Key key, this.videoFile}) : super(key: key);
#override
_MyVideoPlayerState createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
#override
void initState() {
_controller = VideoPlayerController.file(widget.videoFile);
_initializeVideoPlayerFuture = _controller.initialize();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
print(snapshot.connectionState);
if (snapshot.connectionState == ConnectionState.done) {
// Play video once it's loaded
_controller.play();
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: GestureDetector(
onTap: () async {
await _controller.seekTo(Duration.zero);
_controller.play();
},
child: VideoPlayer(_controller),
),
);
} else {
return CircularProgressIndicator();
}
},
);
}
}
The video plays well once the video file is loaded (once the connection state passed to done), however, when I try to tap on the video to replay it a second time, it doesn't replay the video from start. The audio starts playing again, but video doesn't restart playing. Any idea?
EDIT 1
Following #marcosboaventura suggestion, I tried to wrap the calls in a setState to trigger the build method again:
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: GestureDetector(
onTap: () async {
await _controller.seekTo(Duration.zero);
setState(() {
_controller.play();
});
},
child: VideoPlayer(_controller),
),
);
But still the video doesn't replay, only the sound. Any other idea?
I finally found a solution to my issue by calling initialize() again on the controller on the tap event if the video is no longer playing (i.e. the video finished already).
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: GestureDetector(
onTap: () {
if (!_controller.value.isPlaying) {
setState(() {});
_controller.initialize();
}
},
child: VideoPlayer(_controller),
),
);
You need to rebuild the VideoPlayer if you changes anything in video playback. The most simple solution to your case is just fire build method again with a setState call.
/// ... after some code
child: GestureDetector(
onTap: () async {
await _controller.seekTo(Duration.zero);
setState( () {
_controller.play();
} );
},
child: VideoPlayer(_controller),
),
I solve this problem in this way.
GestureDetector(
onTap() {
if(_controller.value.position==_controller.value.duration){
_controller.initialize();
}
}
)
_controller.value.duration store the video duration,
_controller.value.position store the actual position of the video and if the video reaches to the end the _controller.value.position will be equal by _controller.value.duration.
you can do something like this onTap
/// get the duration of the video
final duration = await _controller.position;
/// check if video has ended
if (duration.inSeconds ==_controller.value.duration.inSeconds) {
/// restart the video by setting current position to 0
_controller.seekTo(Duration.zero);
} else {
_controller.value.isPlaying
? _controller.pause()
: _controller.play();
}

Losing data while navigating screens in Flutter

I am new to Flutter and just started to make a tiny little app which takes a list of Top Movies from a server using an async request. and when I tap on top of each one of list items, then it navigates me to another screen to show some details about the movie.
But there is a problem, when I tap on any item to see it's details, inside the details page, when I press back, in the first page, it just loads data again which is not a good user experience. also uses more battery and bandwidth for each request.
I don't know if this is a natural behavior of Flutter to lose data of a Stateful widget after navigating to another screen or there is something wrong with my code.
Can anybody help me with this
This is my code:
import "package:flutter/material.dart";
import "dart:async";
import "dart:convert";
import "package:http/http.dart" as http;
void main() {
runApp(MovieApp());
}
class MovieApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text("Top Movies List",
textDirection: TextDirection.rtl,
style: TextStyle(color: Colors.black87))
]
)
),
body: MoviesList()
)
);
}
}
class MoviesList extends StatefulWidget {
#override
MoviesListState createState() => new MoviesListState();
}
class MoviesListState extends State<MoviesList> {
List moviesList = [];
Future<Map> getData() async {
http.Response response = await http.get(
'http://api.themoviedb.org/3/discover/movie?api_key={api_key}'
);
setState(() {
moviesList = json.decode(response.body)['results'];
});
// return json.decode(response.body);
}
#override
Widget build(BuildContext context) {
getData();
if(moviesList == null) {
return Scaffold(
body: Text('Getting data from server')
);
} else {
return ListView.builder(
itemCount: moviesList.length,
itemBuilder: (context, index){
return Container(
child: ListTile(
title: Text(moviesList[index]['title']),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MovieDetails()),
);
}
)
);
}
);
}
}
}
class MovieDetails extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details')
),
body: Container(
child: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
)
),
);
}
}
Move your getData() method inside the initState() in your State class.
(Remove it from build method)
#override
void initState() {
getData();
super.initState();
}

How to detect the drawer is closed on flutter?

As title. It since that we can detect the drawer is opened, but is this possible to check it is closed or not? Thanks.
I have added this feature in Flutter 2.0.0. Make sure you are using Flutter SDK version >= 2.0.0 to use this.
Simply use a callback in Scaffold
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: NavDrawer(),
onDrawerChanged: (isOpen) {
// write your callback implementation here
print('drawer callback isOpen=$isOpen');
},
endDrawer: NavDrawerEnd(),
onEndDrawerChanged: (isOpen) {
// write your callback implementation here
print('end drawer callback isOpen=$isOpen');
},
body:
...
Pull request merged in 2.0.0: https://github.com/flutter/flutter/pull/67249
Happy coding!
Declare a GlobalKey to reference your drawer:
GlobalKey _drawerKey = GlobalKey();
Put the key in your Drawer:
drawer: Drawer(
key: _drawerKey,
Check if your drawer is visible:
final RenderBox box = _drawerKey.currentContext?.findRenderObject();
if (box != null){
//is visible
} else {
//not visible
}
You can copy paste run full code below
You can wrap Drawer with a StatefulWidget and put callback in initState() and dispose()
initState() will call widget.callback(true); means open
dispose() will call widget.callback(false); means close
Slide also work in this case
code snippet
drawer: CustomDrawer(
callback: (isOpen) {
print("isOpen ${isOpen}");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isDrawerOpen = isOpen;
});
});
},
...
class CustomDrawer extends StatefulWidget {
CustomDrawer({
Key key,
this.elevation = 16.0,
this.child,
this.semanticLabel,
this.callback,
}) : assert(elevation != null && elevation >= 0.0),
super(key: key);
final double elevation;
final Widget child;
final String semanticLabel;
final DrawerCallback callback;
#override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
#override
void initState() {
if (widget.callback != null) {
widget.callback(true);
}
super.initState();
}
#override
void dispose() {
if (widget.callback != null) {
widget.callback(false);
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Drawer(
key: widget.key,
elevation: widget.elevation,
semanticLabel: widget.semanticLabel,
child: widget.child);
}
}
working demo
full code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isDrawerOpen = false;
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: CustomDrawer(
callback: (isOpen) {
print("isOpen ${isOpen}");
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isDrawerOpen = isOpen;
});
});
},
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
// Update the state of the app.
// ...
},
),
],
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Align(
alignment: Alignment.centerRight,
child: Text(
_isDrawerOpen.toString(),
),
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class CustomDrawer extends StatefulWidget {
CustomDrawer({
Key key,
this.elevation = 16.0,
this.child,
this.semanticLabel,
this.callback,
}) : assert(elevation != null && elevation >= 0.0),
super(key: key);
final double elevation;
final Widget child;
final String semanticLabel;
final DrawerCallback callback;
#override
_CustomDrawerState createState() => _CustomDrawerState();
}
class _CustomDrawerState extends State<CustomDrawer> {
#override
void initState() {
if (widget.callback != null) {
widget.callback(true);
}
super.initState();
}
#override
void dispose() {
if (widget.callback != null) {
widget.callback(false);
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Drawer(
key: widget.key,
elevation: widget.elevation,
semanticLabel: widget.semanticLabel,
child: widget.child);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("route test"),
),
body: Text("SecondRoute"));
}
}
You can simply use onDrawerChanged for detecting if the drawer is opened or closed in the Scaffold widget.
Property :
{void Function(bool)? onDrawerChanged}
Type: void Function(bool)?
Optional callback that is called when the Scaffold.drawer is opened or closed.
Example :
#override Widget build(BuildContext context) {
return Scaffold(
onDrawerChanged:(val){
if(val){
setState(() {
//foo bar;
});
}else{
setState(() {
//foo bar;
});
}
},
drawer: Drawer(
child: Container(
)
));
}
When you click a Drawer Item where you will navigate to a new screen, there in the Navigator.push(..) call, you can add a .then(..) clause, and then know when the Drawer item Screen has been popped.
Here is the ListTile for a Drawer item which makes the Navigator.push(..) call when clicked , and the the associated .then(..) callback block:
ListTile(
title: Text('About App'),
onTap: () {
Navigator.push(
_ctxt,
MaterialPageRoute(builder: (context) => AboutScreen()),
).then(
(value) {
print('Drawer callback for About selection');
if (_onReadyCallback != null) {
_onReadyCallback();
}
},
);
}),
_onReadyCallback() represents a Function param you can pass in.
I found this is approach - of leveraging the .then() callback from a .push() call - to be a very useful concept to understand with Flutter in general.
Big thanks to the main 2 answers here:
Force Flutter navigator to reload state when popping
Here's the complete Drawer code:
Drawer drawer = Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
decoration: BoxDecoration(
color: Color(0xFF7FAD5F),
),
child: Text(App.NAME_MENU),
),
ListTile(
title: Text('About App'),
onTap: () {
Navigator.push(
_ctxt,
MaterialPageRoute(builder: (context) => AboutScreen()),
).then(
(value) {
print('Drawer callback for About selection');
if (_onReadyCallback != null) {
_onReadyCallback();
}
},
);
}),
],
),
);
I would recommend that you use this package : https://pub.dev/packages/visibility_detector.
Afterwards you should assign a GlobalKey, like _drawerKey for instance, to the Drawer widget, after which you would be able to detect when the drawer is closed like this:
VisibilityDetector(
key: _drawerKey,
child: Container(),
onVisibilityChanged: (info) {
if (info.visibleFraction == 0.0) {
// drawer not visible.
}
},
)

Resources