Check for a null codec in MultiFrameImageStreamCompleter after calling _emitFrame (#165009)

_emitFrame invokes image listeners. A listener callback could remove all
of the MultiFrameImageStreamCompleter's listeners, resulting in a
_maybeDispose call that destroys the codec.

Fixes https://github.com/flutter/flutter/issues/164944
Fixes https://github.com/flutter/flutter/issues/164537
This commit is contained in:
Jason Simmons 2025-03-12 17:17:30 +00:00 committed by GitHub
parent 212f66fbe7
commit 6316b48bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 1 deletions

View File

@ -1047,6 +1047,10 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_frameDuration = _nextFrame!.duration;
_nextFrame!.image.dispose();
_nextFrame = null;
if (_codec == null) {
// codec was disposed during _emitFrame
return;
}
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
_decodeNextFrameAndSchedule();
@ -1108,7 +1112,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
_nextFrame!.image.dispose();
_nextFrame = null;
_codec!.dispose();
_codec?.dispose();
_codec = null;
return;
}

View File

@ -1076,4 +1076,37 @@ void main() {
areCreateAndDispose,
);
});
testWidgets('MultiFrameImageStreamCompleter image callback can remove listener', (
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,
);
completer.complete(mockCodec);
await tester.idle();
expect(mockCodec.numFramesAsked, 0);
late ImageStreamListener streamListener;
void listener(ImageInfo image, bool synchronousCall) {
addTearDown(image.dispose);
imageStream.removeListener(streamListener);
}
streamListener = ImageStreamListener(listener);
imageStream.addListener(streamListener);
await tester.idle();
expect(mockCodec.numFramesAsked, 1);
final FrameInfo frame = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
mockCodec.completeNextFrame(frame);
await tester.idle();
expect(mockCodec.disposed, true);
});
}