Unverified Commit b6afc16a authored by Amir Hardon's avatar Amir Hardon Committed by GitHub

Make sure _handleAppFrame is only registered once per frame (#30346)

There were 2 possible scenarios in which _handleAppFrame is added more than once as a frame callback. When this happens it is possible that the second invocation will try to access _nextFrame.image when _nextFrame is null and crash. The 2 scenarios are:

Scenario 1

A GIF frame is decoded and a Flutter frame is executed before it's time to show the next GIF frame.
The timer that's waiting for enough time to elapse is invoked, and schedules a callback for the next Flutter frame(here).
Before the next Flutter frame is executed, MultiFrameImageStreamCompleter#removeListener is called followed by ``MultiFrameImageStreamCompleter#addListenerthat is invoking_decodeNextFrameAndSchedule` which is adding `_handleAppFrame` again as a next frame callback.
Scenario 2
removeListener and addListener are called multiple times in succession, every call to addListener can result in another registration of _handleAppFrame to the next Flutter frame callbacks list.

This patch fixes the issue by guarding against a second registration of _handleAppFrame.
parent a83f6ead
...@@ -508,9 +508,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -508,9 +508,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
InformationCollector informationCollector, InformationCollector informationCollector,
}) : assert(codec != null), }) : assert(codec != null),
_informationCollector = informationCollector, _informationCollector = informationCollector,
_scale = scale, _scale = scale {
_framesEmitted = 0,
_timer = null {
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) { codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError( reportError(
context: 'resolving an image codec', context: 'resolving an image codec',
...@@ -531,17 +529,23 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -531,17 +529,23 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
// The requested duration for the current frame; // The requested duration for the current frame;
Duration _frameDuration; Duration _frameDuration;
// How many frames have been emitted so far. // How many frames have been emitted so far.
int _framesEmitted; int _framesEmitted = 0;
Timer _timer; Timer _timer;
// Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
bool _frameCallbackScheduled = false;
void _handleCodecReady(ui.Codec codec) { void _handleCodecReady(ui.Codec codec) {
_codec = codec; _codec = codec;
assert(_codec != null); assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule(); _decodeNextFrameAndSchedule();
} }
}
void _handleAppFrame(Duration timestamp) { void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners) if (!hasListeners)
return; return;
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) { if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
...@@ -557,7 +561,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -557,7 +561,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
} }
final Duration delay = _frameDuration - (timestamp - _shownTimestamp); final Duration delay = _frameDuration - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () { _timer = Timer(delay * timeDilation, () {
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); _scheduleAppFrame();
}); });
} }
...@@ -589,6 +593,14 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -589,6 +593,14 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale)); _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return; return;
} }
_scheduleAppFrame();
}
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
} }
......
...@@ -89,15 +89,39 @@ void main() { ...@@ -89,15 +89,39 @@ void main() {
expect(tester.takeException(), 'failure message'); expect(tester.takeException(), 'failure message');
}); });
testWidgets('First frame decoding starts when codec is ready', (WidgetTester tester) async { testWidgets('Decoding starts when a listener is added after codec is ready', (WidgetTester tester) async {
final Completer<Codec> completer = Completer<Codec>(); final Completer<Codec> completer = Completer<Codec>();
final MockCodec mockCodec = MockCodec(); final MockCodec mockCodec = MockCodec();
mockCodec.frameCount = 1; mockCodec.frameCount = 1;
MultiFrameImageStreamCompleter( final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: completer.future, codec: completer.future,
scale: 1.0, scale: 1.0,
); );
completer.complete(mockCodec);
await tester.idle();
expect(mockCodec.numFramesAsked, 0);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
await tester.idle();
expect(mockCodec.numFramesAsked, 1);
});
testWidgets('Decoding starts when a codec is ready after a listener is added', (WidgetTester tester) async {
final Completer<Codec> completer = Completer<Codec>();
final MockCodec mockCodec = MockCodec();
mockCodec.frameCount = 1;
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: completer.future,
scale: 1.0,
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
await tester.idle();
expect(mockCodec.numFramesAsked, 0);
completer.complete(mockCodec); completer.complete(mockCodec);
await tester.idle(); await tester.idle();
expect(mockCodec.numFramesAsked, 1); expect(mockCodec.numFramesAsked, 1);
...@@ -108,11 +132,13 @@ void main() { ...@@ -108,11 +132,13 @@ void main() {
mockCodec.frameCount = 1; mockCodec.frameCount = 1;
final Completer<Codec> codecCompleter = Completer<Codec>(); final Completer<Codec> codecCompleter = Completer<Codec>();
MultiFrameImageStreamCompleter( final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future, codec: codecCompleter.future,
scale: 1.0, scale: 1.0,
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
// MultiFrameImageStreamCompleter only sets an error handler for the next // MultiFrameImageStreamCompleter only sets an error handler for the next
// frame future after the codec future has completed. // frame future after the codec future has completed.
...@@ -469,4 +495,76 @@ void main() { ...@@ -469,4 +495,76 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
expect(capturedException, 'frame completion error'); expect(capturedException, 'frame completion error');
}); });
testWidgets('remove and add listener ', (WidgetTester tester) async {
final MockCodec mockCodec = MockCodec();
mockCodec.frameCount = 3;
mockCodec.repetitionCount = 0;
final Completer<Codec> codecCompleter = Completer<Codec>();
final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
codec: codecCompleter.future,
scale: 1.0,
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
codecCompleter.complete(mockCodec);
await tester.idle(); // let nextFrameFuture complete
imageStream.removeListener(listener);
imageStream.addListener(listener);
final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
mockCodec.completeNextFrame(frame1);
await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // first animation frame shows on first app frame.
await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
});
// TODO(amirh): enable this once WidgetTester supports flushTimers.
// https://github.com/flutter/flutter/issues/30344
// testWidgets('remove and add listener before a delayed frame is scheduled', (WidgetTester tester) async {
// final MockCodec mockCodec = MockCodec();
// mockCodec.frameCount = 3;
// mockCodec.repetitionCount = 0;
// final Completer<Codec> codecCompleter = Completer<Codec>();
//
// final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
// codec: codecCompleter.future,
// scale: 1.0,
// );
//
// final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
// imageStream.addListener(listener);
//
// codecCompleter.complete(mockCodec);
// await tester.idle();
//
// final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
// final FrameInfo frame2 = FakeFrameInfo(200, 100, const Duration(milliseconds: 400));
// final FrameInfo frame3 = FakeFrameInfo(200, 100, const Duration(milliseconds: 0));
//
// mockCodec.completeNextFrame(frame1);
// await tester.idle(); // let nextFrameFuture complete
// await tester.pump(); // first animation frame shows on first app frame.
//
// mockCodec.completeNextFrame(frame2);
// await tester.pump(const Duration(milliseconds: 100)); // emit 2nd frame.
//
// tester.flushTimers();
//
// imageStream.removeListener(listener);
// imageStream.addListener(listener);
//
// mockCodec.completeNextFrame(frame3);
// await tester.idle(); // let nextFrameFuture complete
//
// await tester.pump();
// });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment