mylisabox / flutter_mjpeg

Flutter widget to show mjpeg stream from URL
BSD 2-Clause "Simplified" License
30 stars 23 forks source link

(HttpException) => Null is not a subtype of type (dynamic) => dynamic #4

Closed estevez-dev closed 3 years ago

estevez-dev commented 4 years ago

First of all thanks for you work on this plugin.

Trying to use version 1.2.5 on Android.

After first frame received and displayed I'm getting the error:

Caught error: type '(HttpException) => Null' is not a subtype of type '(dynamic) => dynamic'
I/flutter (25939): #0      _invokeErrorHandler (dart:async/async_error.dart:18:23)
I/flutter (25939): #1      _HandleErrorStream._handleError (dart:async/stream_pipe.dart:288:9)
I/flutter (25939): #2      _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:170:13)
I/flutter (25939): #3      _rootRunBinary (dart:async/zone.dart:1146:38)
I/flutter (25939): #4      _CustomZone.runBinary (dart:async/zone.dart:1039:19)
I/flutter (25939): #5      _CustomZone.runBinaryGuarded (dart:async/zone.dart:941:7)
I/flutter (25939): #6      _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:357:15)
I/flutter (25939): #7      _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:375:16)
I/flutter (25939): #8      _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:274:7)
I/flutter (25939): #9      _ForwardingStreamSubscription._addError (dart:async/stream_pipe.dart:139:11)
I/flutter (25939): #10     _addErrorWithReplacement (dart:async/stream_pipe.dart:190:8)
I/flutter (25939): #11     _HandleErrorStream._handleError (dart:async/stream_pipe.dart:293:11)
I/flutter (25939): #12     _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:170:13)
I/flutter (25939): #13     _rootRunBinary (dart:asy

I have two different MJPEG streams and getting the same error for both of them. Here is one of them: http://cam2.sigulda.lv/trase.jpg

jaumard commented 4 years ago

Hello :)

This package doesn't pool jpg images every second, it read an mjpeg stream, maybe that's why you have this error. Because the url you give (http://cam2.sigulda.lv/trase.jpg) is just an image not a real stream.

For example this is a real mjpeg stream http://64.74.184.131:8080/mjpg/video.mjpg

estevez-dev commented 4 years ago

Ah sorry, indeed mjpeg stream is formed on my server side. So the server is pooling this jpg every second and creates a stream. I can't give you a link because it needs authorization. Any way I tried to use your stream url and it works fine. So I'm assuming there is something wrong with my streams =( Will check, thanks.

jaumard commented 4 years ago

There is multiple implementation of mjpeg streams, but to have it working with this package it needs this https://github.com/mylisabox/flutter_mjpeg/blob/master/lib/src/mjpeg.dart#L91

So each frame should be separated with trigger, start with soi and end with eoi values. So nothing too complex. Give it a try. maybe the problem come from the authorization too.

Let me know how it goes :) good luck

kylethedeveloper commented 4 years ago

Sorry to bother you with issues but I am getting the same type of error, a little bit different output. You can review it in below.

I followed the stack and it ends with mjpeg.dart, this line. So I commented the _httpClient.close(); line and error disappears. I just wonder what happens when I comment that line and would it cause any issues to my program?

By the way, I can not give a link to my stream however I use this service on my streaming server. I do not use a bare link such as [http://localhost:8000]() but instead I use [http://localhost:8000/stream/0]() on my app to watch the stream. It works fine when I do that.

Here is an example stream that I found on the Internet in case you want to review:
https://appointed-crab-2830.dataplicity.io/stream/0

E/flutter (15164): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: type '(HttpException) => Null' is not a subtype of type '(dynamic) => dynamic'
E/flutter (15164): #0      _invokeErrorHandler (dart:async/async_error.dart:18:23)
E/flutter (15164): #1      _HandleErrorStream._handleError (dart:async/stream_pipe.dart:288:9)
E/flutter (15164): #2      _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:170:13)
E/flutter (15164): #3      _rootRunBinary (dart:async/zone.dart:1146:38)
E/flutter (15164): #4      _CustomZone.runBinary (dart:async/zone.dart:1039:19)
E/flutter (15164): #5      _CustomZone.runBinaryGuarded (dart:async/zone.dart:941:7)
E/flutter (15164): #6      _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:357:15)
E/flutter (15164): #7      _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:375:16)
E/flutter (15164): #8      _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:274:7)
E/flutter (15164): #9      _ForwardingStreamSubscription._addError (dart:async/stream_pipe.dart:139:11)
E/flutter (15164): #10     _addErrorWithReplacement (dart:async/stream_pipe.dart:190:8)
E/flutter (15164): #11     _HandleErrorStream._handleError (dart:async/stream_pipe.dart:293:11)
E/flutter (15164): #12     _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:170:13)
E/flutter (15164): #13     _rootRunBinary (dart:async/zone.dart:1146:38)
E/flutter (15164): #14     _CustomZone.runBinary (dart:async/zone.dart:1039:19)
E/flutter (15164): #15     _CustomZone.runBinaryGuarded (dart:async/zone.dart:941:7)
E/flutter (15164): #16     _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:357:15)
E/flutter (15164): #17     _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:375:16)
E/flutter (15164): #18     _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:274:7)
E/flutter (15164): #19     _SyncStreamControllerDispatch._sendError (dart:async/stream_controller.dart:770:19)
E/flutter (15164): #20     _StreamController._addError (dart:async/stream_controller.dart:650:7)
E/flutter (15164): #21     _StreamController.addError (dart:async/stream_controller.dart:602:5)
E/flutter (15164): #22     _HttpParser._onDone (dart:_http/http_parser.dart:824:25)
E/flutter (15164): #23     _rootRun (dart:async/zone.dart:1122:38)
E/flutter (15164): #24     _CustomZone.run (dart:async/zone.dart:1023:19)
E/flutter (15164): #25     _CustomZone.runGuarded (dart:async/zone.dart:925:7)
E/flutter (15164): #26     _BufferingStreamSubscription._sendDone.sendDone (dart:async/stream_impl.dart:391:13)
E/flutter (15164): #27     _BufferingStreamSubscription._sendDone (dart:async/stream_impl.dart:401:15)
E/flutter (15164): #28     _BufferingStreamSubscription._close (dart:async/stream_impl.dart:285:7)
E/flutter (15164): #29     _SyncStreamControllerDispatch._sendDone (dart:async/stream_controller.dart:774:19)
E/flutter (15164): #30     _StreamController._closeUnchecked (dart:async/stream_controller.dart:631:7)
E/flutter (15164): #31     _StreamController.close (dart:async/stream_controller.dart:624:5)
E/flutter (15164): #32     _Socket._onDone (dart:io-patch/socket_patch.dart:1866:19)
E/flutter (15164): #33     _rootRun (dart:async/zone.dart:1122:38)
E/flutter (15164): #34     _CustomZone.run (dart:async/zone.dart:1023:19)
E/flutter (15164): #35     _CustomZone.runGuarded (dart:async/zone.dart:925:7)
E/flutter (15164): #36     _BufferingStreamSubscription._sendDone.sendDone (dart:async/stream_impl.dart:391:13)
E/flutter (15164): #37     _BufferingStreamSubscription._sendDone (dart:async/stream_impl.dart:401:15)
E/flutter (15164): #38     _BufferingStreamSubscription._close (dart:async/stream_impl.dart:285:7)
E/flutter (15164): #39     _SyncStreamControllerDispatch._sendDone (dart:async/stream_controller.dart:774:19)
E/flutter (15164): #40     _StreamController._closeUnchecked (dart:async/stream_controller.dart:631:7)
E/flutter (15164): #41     _StreamController.close (dart:async/stream_controller.dart:624:5)
E/flutter (15164): #42     _RawSecureSocket._close (dart:io/secure_socket.dart:650:17)
E/flutter (15164): #43     _RawSecureSocket.shutdown (dart:io/secure_socket.dart:672:9)
E/flutter (15164): #44     _RawSecureSocket.close (dart:io/secure_socket.dart:625:5)
E/flutter (15164): #45     _Socket._closeRawSocket (dart:io-patch/socket_patch.dart:1822:9)
E/flutter (15164): #46     _Socket.destroy (dart:io-patch/socket_patch.dart:1754:5)
E/flutter (15164): #47     _HttpClientConnection.destroy (dart:_http/http_impl.dart:1870:13)
E/flutter (15164): #48     _ConnectionTarget.close (dart:_http/http_impl.dart:2033:11)
E/flutter (15164): #49     _HttpClient._closeConnections (dart:_http/http_impl.dart:2379:24)
E/flutter (15164): #50     _HttpClient.close (dart:_http/http_impl.dart:2227:5)
E/flutter (15164): #51     IOClient.close (package:http/src/io_client.dart:73:14)
E/flutter (15164): #52     _StreamManager.dispose (package:flutter_mjpeg/src/mjpeg.dart:130:17)
E/flutter (15164): #53 
jaumard commented 4 years ago

If you're not doing _httpClient.close(); it means the http stream is never closed, so you're leaking an http socket that will stay open forever. And next time you'll create another stream that will stay open too and at some point you're app will be slow or crash cause of memory.

with https://appointed-crab-2830.dataplicity.io/stream/0 you have the problem ? when exactly ?

kylethedeveloper commented 4 years ago

I route to my LiveStream page, Mjpeg works fine with https://appointed-crab-2830.dataplicity.io/stream/0 url (in my case I have another url with exactly the same structure, this is just an example)

I can watch it live. When I pop back to my home screen, I get this error.

I understand that I need to close the http stream but when I uncomment the line that I mentioned, I get the error. What could be the cause? Or should I just ignore it?

YowFung commented 3 years ago

It looks like the problem is in the dispose() function.

https://github.com/mylisabox/flutter_mjpeg/blob/01c3cd4c83544ee07feb4b37a188d00740e4267a/lib/src/mjpeg.dart#L132

Note: In both cases trying to establish a new connection after calling [close] will throw an exception.

image

YowFung commented 3 years ago

The _StreamManager.dispose() is a future.

The first argument of useEffect needs to return a function as a clean function. https://github.com/mylisabox/flutter_mjpeg/blob/01c3cd4c83544ee07feb4b37a188d00740e4267a/lib/src/mjpeg.dart#L70

In definition of useEffect. When cleaning is needed, it is just called as a normal function. It does not wait for future execution to complete. image

Go back to _StreamManager.dispose(). It probably hasn't closed yet. But at the same time, the following code is ready to create a new HTTP connection. So the program went wrong. https://github.com/mylisabox/flutter_mjpeg/blob/01c3cd4c83544ee07feb4b37a188d00740e4267a/lib/src/mjpeg.dart#L64

In short, it‘s the problem that asynchrony and state are not well managed.

jaumard commented 3 years ago

Interesting! To me useEffect callback is called when widget is disposed, so it mean the StreamManager will not be called anymore anyway.

useMemoized should then give me another instance of StreamManager so I should in fact be good.

that was my asumpsions but I'll double check that once I have time. I also happy to review and merged PR if you can reproduce and fix it for sure. On my side last time I checked I wasn't able to reproduce it

YowFung commented 3 years ago

image

image

image

In my opinion, when any of the stream, isLive, visible or timeout was been changed, the manager will be re-instantiated, resulting in the callback in useEffect wall be called again.

jaumard commented 3 years ago

Yes sorry I tried to type quickly because I was on my phone, but that's exactly it, meaning that useEffet is always disposing an old one, so we don't care if that one take time to be disposed as it's not used anymore. We basically always deal with a new one when a param is changing, so taking time to dispose shouldn't be a problem (at least in my mind :D). But again when I got time I'll dig more into this !

YowFung commented 3 years ago

I didn't reproduce this error log. But I've proved that it does create a new HTTP connection before closing the previous one.

Add print lines:

final manager = useMemoized(() {
   print(">>>>>> [${DateTime.now()}] Create a new manager instance.");
   return _StreamManager(stream, isLive && visible.visible, headers, timeout);
}, [stream, isLive, visible.visible]);
useEffect(() {
   print(">>>>>> [${DateTime.now()}] call useEffect.");
   errorState.value = null;
   manager.updateStream(context, image, errorState);
   return manager.dispose;
}, [manager]);
Future<void> dispose() async {
   print(">>>>>> [${DateTime.now()}] dispose begin.");
   if (_subscription != null) {
     await _subscription!.cancel();
     _subscription = null;
   }
   _httpClient.close();
   print(">>>>>> [${DateTime.now()}] dispose end.");
}
try {
     print(">>>>>> [${DateTime.now()}] Create a new HTTP connection.");
     final request = Request("GET", Uri.parse(stream));

Running example application, and tap the Toggle button. And then get the following log:

flutter: >>>>>> [2021-06-10 10:44:21.484232] Create a new manager instance.
flutter: >>>>>> [2021-06-10 10:44:21.489948] call useEffect.
flutter: >>>>>> [2021-06-10 10:44:21.493157] Create a new HTTP connection.

flutter: >>>>>> [2021-06-10 10:44:33.749914] Create a new manager instance.
flutter: >>>>>> [2021-06-10 10:44:33.750066] call useEffect.
flutter: >>>>>> [2021-06-10 10:44:33.750125] Create a new HTTP connection.
flutter: >>>>>> [2021-06-10 10:44:33.751426] dispose begin.
flutter: >>>>>> [2021-06-10 10:44:33.765322] dispose end.

flutter: >>>>>> [2021-06-10 10:44:42.786749] dispose begin.
flutter: >>>>>> [2021-06-10 10:44:42.787803] dispose end.
jaumard commented 3 years ago

Your logs are correct, but they don't show the instance they are called on.

This is what I expect:

flutter: >>>>>> [2021-06-10 10:44:21.484232] Create a new manager instance. Instance 1
flutter: >>>>>> [2021-06-10 10:44:21.489948] call useEffect. Instance 1
flutter: >>>>>> [2021-06-10 10:44:21.493157] Create a new HTTP connection. Instance 1

flutter: >>>>>> [2021-06-10 10:44:33.749914] Create a new manager instance. Instance 2
flutter: >>>>>> [2021-06-10 10:44:33.750066] call useEffect. Instance 2
flutter: >>>>>> [2021-06-10 10:44:33.750125] Create a new HTTP connection. Instance 2
flutter: >>>>>> [2021-06-10 10:44:33.751426] dispose begin. Instance 1
flutter: >>>>>> [2021-06-10 10:44:33.765322] dispose end. Instance 1

flutter: >>>>>> [2021-06-10 10:44:42.786749] dispose begin. Instance 2
flutter: >>>>>> [2021-06-10 10:44:42.787803] dispose end. Instance 2

Basically it's totally fine that a new connection is created before the dispose if it's not on the same instance. Because each instance have his own HTTP client. So the disposed httpClient is actually not used when it's disposed.

I've done some research and found this: https://github.com/dart-lang/http/issues/418 Look like it was an issue on dart side that is now fixed. So I suggest just to close this one.

Agree?

YowFung commented 3 years ago

Hi @jaumard, You're right. Sorry, I misunderstood it.

jaumard commented 3 years ago

No problem happy to be pushed on that ^^ because I could have been wrong :D Thanks for that!