image_stream.dart 26.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6 7
import 'dart:ui' as ui show Image, Codec, FrameInfo;
import 'dart:ui' show hashValues;
8 9

import 'package:flutter/foundation.dart';
10
import 'package:flutter/scheduler.dart';
11

12
/// A [dart:ui.Image] object with its corresponding scale.
13 14 15
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
16
@immutable
17 18 19 20
class ImageInfo {
  /// Creates an [ImageInfo] object for the given image and scale.
  ///
  /// Both the image and the scale must not be null.
21
  const ImageInfo({ @required this.image, this.scale = 1.0 })
22 23
    : assert(image != null),
      assert(scale != null);
24 25 26 27 28 29 30 31 32 33 34 35 36 37

  /// The raw image pixels.
  ///
  /// This is the object to pass to the [Canvas.drawImage],
  /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
  /// the image.
  final ui.Image image;

  /// The linear scale factor for drawing this image at its intended size.
  ///
  /// The scale factor applies to the width and the height.
  ///
  /// For example, if this is 2.0 it means that there are four image pixels for
  /// every one logical pixel, and the image's actual width and height (as given
38
  /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are double the
39 40 41 42 43
  /// height and width that should be used when painting the image (e.g. in the
  /// arguments given to [Canvas.drawImage]).
  final double scale;

  @override
44
  String toString() => '$image @ ${debugFormatDouble(scale)}x';
45 46 47 48 49 50 51 52

  @override
  int get hashCode => hashValues(image, scale);

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
53 54 55
    return other is ImageInfo
        && other.image == image
        && other.scale == scale;
56
  }
57 58
}

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
/// Interface for receiving notifications about the loading of an image.
///
/// This class overrides `operator ==` and `hashCode` to compare the individual
/// callbacks in the listener, meaning that if you add an instance of this class
/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
/// _different_ instance of this class when you remove the listener, and the
/// listener will be properly removed as long all associated callbacks are
/// equal.
///
/// Used by [ImageStream] and [ImageStreamCompleter].
@immutable
class ImageStreamListener {
  /// Creates a new [ImageStreamListener].
  ///
  /// The [onImage] parameter must not be null.
  const ImageStreamListener(
    this.onImage, {
76
    this.onChunk,
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    this.onError,
  }) : assert(onImage != null);

  /// Callback for getting notified that an image is available.
  ///
  /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
  /// that drives the notifications fires multiple times). An example of such a
  /// case would be an image with multiple frames within it (such as an animated
  /// GIF).
  ///
  /// For more information on how to interpret the parameters to the callback,
  /// see the documentation on [ImageListener].
  ///
  /// See also:
  ///
  ///  * [onError], which will be called instead of [onImage] if an error occurs
  ///    during loading.
  final ImageListener onImage;

96 97 98 99 100 101 102 103 104 105 106 107 108
  /// Callback for getting notified when a chunk of bytes has been received
  /// during the loading of the image.
  ///
  /// This callback may fire many times (e.g. when used with a [NetworkImage],
  /// where the image bytes are loaded incrementally over the wire) or not at
  /// all (e.g. when used with a [MemoryImage], where the image bytes are
  /// already available in memory).
  ///
  /// This callback may also continue to fire after the [onImage] callback has
  /// fired (e.g. for multi-frame images that continue to load after the first
  /// frame is available).
  final ImageChunkListener onChunk;

109 110 111 112 113 114 115
  /// Callback for getting notified when an error occurs while loading an image.
  ///
  /// If an error occurs during loading, [onError] will be called instead of
  /// [onImage].
  final ImageErrorListener onError;

  @override
116
  int get hashCode => hashValues(onImage, onChunk, onError);
117 118 119 120 121

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
122 123 124 125
    return other is ImageStreamListener
        && other.onImage == onImage
        && other.onChunk == onChunk
        && other.onError == onError;
126 127 128
  }
}

129 130
/// Signature for callbacks reporting that an image is available.
///
131
/// Used in [ImageStreamListener].
132 133
///
/// The `synchronousCall` argument is true if the listener is being invoked
134
/// during the call to `addListener`. This can be useful if, for example,
135 136 137 138
/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
/// frame is requested if the call was asynchronous (after the current frame)
/// and no rendering frame is requested if the call was synchronous (within the
/// same stack frame as the call to [ImageStream.addListener]).
139
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
140

141 142 143 144 145
/// Signature for listening to [ImageChunkEvent] events.
///
/// Used in [ImageStreamListener].
typedef ImageChunkListener = void Function(ImageChunkEvent event);

146 147
/// Signature for reporting errors when resolving images.
///
148 149
/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
/// [precacheImage], to report errors.
150
typedef ImageErrorListener = void Function(dynamic exception, StackTrace stackTrace);
151

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
/// An immutable notification of image bytes that have been incrementally loaded.
///
/// Chunk events represent progress notifications while an image is being
/// loaded (e.g. from disk or over the network).
///
/// See also:
///
///  * [ImageChunkListener], the means by which callers get notified of
///    these events.
@immutable
class ImageChunkEvent extends Diagnosticable {
  /// Creates a new chunk event.
  const ImageChunkEvent({
    @required this.cumulativeBytesLoaded,
    @required this.expectedTotalBytes,
  }) : assert(cumulativeBytesLoaded >= 0),
       assert(expectedTotalBytes == null || expectedTotalBytes >= 0);

  /// The number of bytes that have been received across the wire thus far.
  final int cumulativeBytesLoaded;

  /// The expected number of bytes that need to be received to finish loading
  /// the image.
  ///
  /// This value is not necessarily equal to the expected _size_ of the image
  /// in bytes, as the bytes required to load the image may be compressed.
  ///
  /// This value will be null if the number is not known in advance.
  ///
  /// When this value is null, the chunk event may still be useful as an
  /// indication that data is loading (and how much), but it cannot represent a
  /// loading completion percentage.
  final int expectedTotalBytes;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
    properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
  }
}

194 195
/// A handle to an image resource.
///
196
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
197 198 199 200 201 202 203 204
/// (together represented by an [ImageInfo] object). The underlying image object
/// might change over time, either because the image is animating or because the
/// underlying image resource was mutated.
///
/// ImageStream objects can also represent an image that hasn't finished
/// loading.
///
/// ImageStream objects are backed by [ImageStreamCompleter] objects.
205 206 207 208 209
///
/// See also:
///
///  * [ImageProvider], which has an example that includes the use of an
///    [ImageStream] in a [Widget].
210
class ImageStream extends Diagnosticable {
211 212 213 214 215 216 217 218 219 220 221
  /// Create an initially unbound image stream.
  ///
  /// Once an [ImageStreamCompleter] is available, call [setCompleter].
  ImageStream();

  /// The completer that has been assigned to this image stream.
  ///
  /// Generally there is no need to deal with the completer directly.
  ImageStreamCompleter get completer => _completer;
  ImageStreamCompleter _completer;

222
  List<ImageStreamListener> _listeners;
223 224 225 226 227

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  ///
  /// This is usually done automatically by the [ImageProvider] that created the
  /// [ImageStream].
228 229 230 231
  ///
  /// This method can only be called once per stream. To have an [ImageStream]
  /// represent multiple images over time, assign it a completer that
  /// completes several images in succession.
232 233 234 235
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
236
      final List<ImageStreamListener> initialListeners = _listeners;
237
      _listeners = null;
238
      initialListeners.forEach(_completer.addListener);
239 240 241
    }
  }

242
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
243 244
  /// object is available. If a concrete image is already available, this object
  /// will call the listener synchronously.
245 246 247 248
  ///
  /// If the assigned [completer] completes multiple images over its lifetime,
  /// this listener will fire multiple times.
  ///
249
  /// {@template flutter.painting.imageStream.addListener}
250 251 252 253
  /// The listener will be passed a flag indicating whether a synchronous call
  /// occurred. If the listener is added within a render object paint function,
  /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
  /// a paint.
254
  ///
255 256 257 258 259 260
  /// If a duplicate `listener` is registered N times, then it will be called N
  /// times when the image stream completes (whether because a new image is
  /// available or because an error occurs). Likewise, to remove all instances
  /// of the listener, [removeListener] would need to called N times as well.
  /// {@endtemplate}
  void addListener(ImageStreamListener listener) {
261
    if (_completer != null)
262 263 264
      return _completer.addListener(listener);
    _listeners ??= <ImageStreamListener>[];
    _listeners.add(listener);
265 266
  }

267 268 269 270 271
  /// Stops listening for events from this stream's [ImageStreamCompleter].
  ///
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
  void removeListener(ImageStreamListener listener) {
272 273 274
    if (_completer != null)
      return _completer.removeListener(listener);
    assert(_listeners != null);
275
    for (int i = 0; i < _listeners.length; i += 1) {
276
      if (_listeners[i] == listener) {
277
        _listeners.removeAt(i);
278
        break;
279 280
      }
    }
281 282 283 284 285
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
286 287
  /// This can be used to avoid un-registering and re-registering listeners
  /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
288 289 290 291 292 293
  /// [ImageProvider].
  ///
  /// The key may change once in the lifetime of the object. When it changes, it
  /// will go from being different than other [ImageStream]'s keys to
  /// potentially being the same as others'. No notification is sent when this
  /// happens.
294
  Object get key => _completer ?? this;
295

296 297 298
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
299
    properties.add(ObjectFlagProperty<ImageStreamCompleter>(
300 301 302 303 304
      'completer',
      _completer,
      ifPresent: _completer?.toStringShort(),
      ifNull: 'unresolved',
    ));
305
    properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
306 307 308 309
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
      ifNull: 'no listeners',
310
      level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
311 312
    ));
    _completer?.debugFillProperties(properties);
313 314 315
  }
}

316
/// Base class for those that manage the loading of [dart:ui.Image] objects for
317 318
/// [ImageStream]s.
///
319 320 321 322
/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
/// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible.
abstract class ImageStreamCompleter extends Diagnosticable {
323
  final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
324 325
  ImageInfo _currentImage;
  FlutterErrorDetails _currentError;
326

327 328 329 330 331 332 333 334 335 336 337
  /// Whether any listeners are currently registered.
  ///
  /// Clients should not depend on this value for their behavior, because having
  /// one listener's logic change when another listener happens to start or stop
  /// listening will lead to extremely hard-to-track bugs. Subclasses might use
  /// this information to determine whether to do any work when there are no
  /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
  /// to determine when to iterate through frames of an animated image.
  ///
  /// Typically this is used by overriding [addListener], checking if
  /// [hasListeners] is false before calling `super.addListener()`, and if so,
338 339 340 341
  /// starting whatever work is needed to determine when to notify listeners;
  /// and similarly, by overriding [removeListener], checking if [hasListeners]
  /// is false after calling `super.removeListener()`, and if so, stopping that
  /// same work.
342 343 344
  @protected
  bool get hasListeners => _listeners.isNotEmpty;

345
  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
346 347
  /// object is available or an error is reported. If a concrete image is
  /// already available, or if an error has been already reported, this object
348
  /// will notify the listener synchronously.
349
  ///
350
  /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
351
  /// this listener's [ImageStreamListener.onImage] will fire multiple times.
352
  ///
353 354 355
  /// {@macro flutter.painting.imageStream.addListener}
  void addListener(ImageStreamListener listener) {
    _listeners.add(listener);
356
    if (_currentImage != null) {
357
      try {
358
        listener.onImage(_currentImage, true);
359
      } catch (exception, stack) {
360
        reportError(
361
          context: ErrorDescription('by a synchronously-called image listener'),
362 363 364
          exception: exception,
          stack: stack,
        );
365 366
      }
    }
367
    if (_currentError != null && listener.onError != null) {
368
      try {
369
        listener.onError(_currentError.exception, _currentError.stack);
370 371
      } catch (exception, stack) {
        FlutterError.reportError(
372
          FlutterErrorDetails(
373 374
            exception: exception,
            library: 'image resource service',
375
            context: ErrorDescription('by a synchronously-called image error listener'),
376 377 378 379
            stack: stack,
          ),
        );
      }
380
    }
381 382
  }

383
  /// Stops the specified [listener] from receiving image stream events.
384
  ///
385 386 387
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
  void removeListener(ImageStreamListener listener) {
388
    for (int i = 0; i < _listeners.length; i += 1) {
389
      if (_listeners[i] == listener) {
390
        _listeners.removeAt(i);
391
        break;
392 393
      }
    }
394 395 396 397 398
  }

  /// Calls all the registered listeners to notify them of a new image.
  @protected
  void setImage(ImageInfo image) {
399
    _currentImage = image;
400 401
    if (_listeners.isEmpty)
      return;
402
    // Make a copy to allow for concurrent modification.
403 404 405
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.from(_listeners);
    for (ImageStreamListener listener in localListeners) {
406
      try {
407
        listener.onImage(image, false);
408
      } catch (exception, stack) {
409
        reportError(
410
          context: ErrorDescription('by an image listener'),
411 412 413
          exception: exception,
          stack: stack,
        );
414 415 416 417
      }
    }
  }

418 419 420
  /// Calls all the registered error listeners to notify them of an error that
  /// occurred while resolving the image.
  ///
421 422
  /// If no error listeners (listeners with an [ImageStreamListener.onError]
  /// specified) are attached, a [FlutterError] will be reported instead.
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
  ///
  /// The `context` should be a string describing where the error was caught, in
  /// a form that will make sense in English when following the word "thrown",
  /// as in "thrown while obtaining the image from the network" (for the context
  /// "while obtaining the image from the network").
  ///
  /// The `exception` is the error being reported; the `stack` is the
  /// [StackTrace] associated with the exception.
  ///
  /// The `informationCollector` is a callback (of type [InformationCollector])
  /// that is called when the exception is used by [FlutterError.reportError].
  /// It is used to obtain further details to include in the logs, which may be
  /// expensive to collect, and thus should only be collected if the error is to
  /// be logged in the first place.
  ///
  /// The `silent` argument causes the exception to not be reported to the logs
  /// in release builds, if passed to [FlutterError.reportError]. (It is still
  /// sent to error handlers.) It should be set to true if the error is one that
  /// is expected to be encountered in release builds, for example network
  /// errors. That way, logs on end-user devices will not have spurious
  /// messages, but errors during development will still be reported.
  ///
  /// See [FlutterErrorDetails] for further details on these values.
446 447
  @protected
  void reportError({
448
    DiagnosticsNode context,
449 450 451 452 453
    dynamic exception,
    StackTrace stack,
    InformationCollector informationCollector,
    bool silent = false,
  }) {
454
    _currentError = FlutterErrorDetails(
455 456 457
      exception: exception,
      stack: stack,
      library: 'image resource service',
458 459 460 461 462
      context: context,
      informationCollector: informationCollector,
      silent: silent,
    );

463
    // Make a copy to allow for concurrent modification.
464 465 466 467
    final List<ImageErrorListener> localErrorListeners = _listeners
        .map<ImageErrorListener>((ImageStreamListener listener) => listener.onError)
        .where((ImageErrorListener errorListener) => errorListener != null)
        .toList();
468 469 470 471 472 473 474 475 476

    if (localErrorListeners.isEmpty) {
      FlutterError.reportError(_currentError);
    } else {
      for (ImageErrorListener errorListener in localErrorListeners) {
        try {
          errorListener(exception, stack);
        } catch (exception, stack) {
          FlutterError.reportError(
477
            FlutterErrorDetails(
478
              context: ErrorDescription('when reporting an error to an image listener'),
479 480 481 482 483 484 485 486
              library: 'image resource service',
              exception: exception,
              stack: stack,
            ),
          );
        }
      }
    }
487 488 489 490
  }

  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
491 492 493
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
494
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
495
    description.add(ObjectFlagProperty<List<ImageStreamListener>>(
496 497 498 499
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
    ));
500 501 502
  }
}

503
/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
504 505 506 507 508 509 510
/// with only one frame).
class OneFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a manager for one-frame [ImageStream]s.
  ///
  /// The image resource awaits the given [Future]. When the future resolves,
  /// it notifies the [ImageListener]s that have been registered with
  /// [addListener].
511 512 513 514 515 516 517 518 519
  ///
  /// The [InformationCollector], if provided, is invoked if the given [Future]
  /// resolves with an error, and can be used to supplement the reported error
  /// message (for example, giving the image's URL).
  ///
  /// Errors are reported using [FlutterError.reportError] with the `silent`
  /// argument on [FlutterErrorDetails] set to true, meaning that by default the
  /// message is only dumped to the console in debug mode (see [new
  /// FlutterErrorDetails]).
520
  OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector informationCollector })
521
      : assert(image != null) {
522
    image.then<void>(setImage, onError: (dynamic error, StackTrace stack) {
523
      reportError(
524
        context: ErrorDescription('resolving a single-frame image stream'),
525 526
        exception: error,
        stack: stack,
527 528
        informationCollector: informationCollector,
        silent: true,
529
      );
530
    });
531 532
  }
}
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556

/// Manages the decoding and scheduling of image frames.
///
/// New frames will only be emitted while there are registered listeners to the
/// stream (registered with [addListener]).
///
/// This class deals with 2 types of frames:
///
///  * image frames - image frames of an animated image.
///  * app frames - frames that the flutter engine is drawing to the screen to
///    show the app GUI.
///
/// For single frame images the stream will only complete once.
///
/// For animated images, this class eagerly decodes the next image frame,
/// and notifies the listeners that a new frame is ready on the first app frame
/// that is scheduled after the image frame duration has passed.
///
/// Scheduling new timers only from scheduled app frames, makes sure we pause
/// the animation when the app is not visible (as new app frames will not be
/// scheduled).
///
/// See the following timeline example:
///
557 558 559 560 561 562 563 564
///     | Time | Event                                      | Comment                   |
///     |------|--------------------------------------------|---------------------------|
///     | t1   | App frame scheduled (image frame A posted) |                           |
///     | t2   | App frame scheduled                        |                           |
///     | t3   | App frame scheduled                        |                           |
///     | t4   | Image frame B decoded                      |                           |
///     | t5   | App frame scheduled                        | t5 - t1 < frameB_duration |
///     | t6   | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
565 566 567 568 569 570
///
class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a image stream completer.
  ///
  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
571 572 573 574 575 576 577 578 579 580
  /// The `codec` parameter is a future for an initialized [ui.Codec] that will
  /// be used to decode the image.
  ///
  /// The `scale` parameter is the linear scale factor for drawing this frames
  /// of this image at their intended size.
  ///
  /// The `chunkEvents` parameter is an optional stream of notifications about
  /// the loading progress of the image. If this stream is provided, the events
  /// produced by the stream will be delivered to registered [ImageChunkListener]s
  /// (see [addListener]).
581 582 583
  MultiFrameImageStreamCompleter({
    @required Future<ui.Codec> codec,
    @required double scale,
584
    Stream<ImageChunkEvent> chunkEvents,
585
    InformationCollector informationCollector,
586 587
  }) : assert(codec != null),
       _informationCollector = informationCollector,
588
       _scale = scale {
589
    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
590
      reportError(
591
        context: ErrorDescription('resolving an image codec'),
592 593 594 595
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
596
      );
597
    });
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
    if (chunkEvents != null) {
      chunkEvents.listen(
        (ImageChunkEvent event) {
          if (hasListeners) {
            // Make a copy to allow for concurrent modification.
            final List<ImageChunkListener> localListeners = _listeners
                .map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
                .where((ImageChunkListener chunkListener) => chunkListener != null)
                .toList();
            for (ImageChunkListener listener in localListeners) {
              listener(event);
            }
          }
        }, onError: (dynamic error, StackTrace stack) {
          reportError(
            context: ErrorDescription('loading an image'),
            exception: error,
            stack: stack,
            informationCollector: informationCollector,
            silent: true,
          );
        },
      );
    }
622 623 624 625 626 627 628 629 630 631 632
  }

  ui.Codec _codec;
  final double _scale;
  final InformationCollector _informationCollector;
  ui.FrameInfo _nextFrame;
  // When the current was first shown.
  Duration _shownTimestamp;
  // The requested duration for the current frame;
  Duration _frameDuration;
  // How many frames have been emitted so far.
633
  int _framesEmitted = 0;
634 635
  Timer _timer;

636 637 638
  // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  bool _frameCallbackScheduled = false;

639
  void _handleCodecReady(ui.Codec codec) {
640
    _codec = codec;
641 642
    assert(_codec != null);

643 644 645
    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
646 647 648
  }

  void _handleAppFrame(Duration timestamp) {
649
    _frameCallbackScheduled = false;
650
    if (!hasListeners)
651 652
      return;
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
653
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
654 655 656 657 658 659 660 661 662 663
      _shownTimestamp = timestamp;
      _frameDuration = _nextFrame.duration;
      _nextFrame = null;
      final int completedCycles = _framesEmitted ~/ _codec.frameCount;
      if (_codec.repetitionCount == -1 || completedCycles <= _codec.repetitionCount) {
        _decodeNextFrameAndSchedule();
      }
      return;
    }
    final Duration delay = _frameDuration - (timestamp - _shownTimestamp);
664
    _timer = Timer(delay * timeDilation, () {
665
      _scheduleAppFrame();
666 667 668 669 670 671 672 673 674 675 676 677
    });
  }

  bool _isFirstFrame() {
    return _frameDuration == null;
  }

  bool _hasFrameDurationPassed(Duration timestamp) {
    assert(_shownTimestamp != null);
    return timestamp - _shownTimestamp >= _frameDuration;
  }

678
  Future<void> _decodeNextFrameAndSchedule() async {
679 680 681
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
682
      reportError(
683
        context: ErrorDescription('resolving an image frame'),
684 685 686 687 688
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
689 690 691 692 693
      return;
    }
    if (_codec.frameCount == 1) {
      // This is not an animated image, just return it and don't schedule more
      // frames.
694
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
695 696
      return;
    }
697 698 699 700 701 702 703 704
    _scheduleAppFrame();
  }

  void _scheduleAppFrame() {
    if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
705 706 707 708 709 710 711 712 713
    SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
  }

  void _emitFrame(ImageInfo imageInfo) {
    setImage(imageInfo);
    _framesEmitted += 1;
  }

  @override
714
  void addListener(ImageStreamListener listener) {
715
    if (!hasListeners && _codec != null)
716
      _decodeNextFrameAndSchedule();
717
    super.addListener(listener);
718 719 720
  }

  @override
721
  void removeListener(ImageStreamListener listener) {
722
    super.removeListener(listener);
723
    if (!hasListeners) {
724 725 726 727 728
      _timer?.cancel();
      _timer = null;
    }
  }
}