capture method

Future<PatchbayInvocation> capture(
  1. PatchbayCaptureRequestWire request, {
  2. required String requestId,
})

Implementation

Future<PatchbayInvocation> capture(
  PatchbayCaptureRequestWire request, {
  required String requestId,
}) async {
  final Duration timeout = request.timeoutMs == null
      ? defaultTimeout
      : Duration(milliseconds: request.timeoutMs!);
  final double pixelRatio = request.pixelRatio?.toDouble() ?? 1;
  final int requestedFrames = request.afterFrames ?? 1;
  final List<String> outOfRange = <String>[
    if (timeout <= Duration.zero || timeout > const Duration(seconds: 30))
      'timeoutMs',
    if (!pixelRatio.isFinite || pixelRatio <= 0 || pixelRatio > maxPixelRatio)
      'pixelRatio',
    if (requestedFrames <= 0 || requestedFrames > maxAfterFrames)
      'afterFrames',
  ];
  if (outOfRange.isNotEmpty) {
    // Which bound was crossed, and what the bound is. Both are declared in
    // the catalog already; repeating them here saves the caller a round trip
    // through the descriptor to find out why a plausible number was refused.
    return _reject(
      requestId,
      'invalidCaptureArguments',
      details: <String, Object?>{
        'invalid': outOfRange,
        'maxTimeoutMs': const Duration(seconds: 30).inMilliseconds,
        'maxPixelRatio': maxPixelRatio,
        'maxAfterFrames': maxAfterFrames,
      },
    );
  }
  if (!_isAppResumed()) {
    return _reject(
      requestId,
      'captureLifecycleNotResumed',
      details: patchbayLifecycleDetails(_lifecycleState),
    );
  }
  final _CaptureResolution before = _resolve(request);
  if (!before.resolved) return _rejectResolution(requestId, before);

  final Set<String> gates = <String>{..._gateIds, ...before.gates};
  final PatchbayGateRejection? gate = await _gates.evaluate(gates);
  if (gate != null) {
    return _reject(
      requestId,
      gate.code,
      details: <String, Object?>{'gateId': gate.gateId},
      notice: gate.notice,
    );
  }
  if (!_isAppResumed()) {
    return _reject(
      requestId,
      'captureLifecycleNotResumed',
      details: patchbayLifecycleDetails(_lifecycleState),
    );
  }
  final DateTime deadline = DateTime.now().add(timeout);
  final int observedFrames = await _frames.framesBefore(
    deadline,
    requestedFrames,
  );
  if (observedFrames != requestedFrames) {
    return _reject(
      requestId,
      'captureFrameTimeout',
      details: <String, Object?>{
        'requestedFrames': requestedFrames,
        'observedFrames': observedFrames,
      },
    );
  }
  if (!_isAppResumed()) {
    return _reject(
      requestId,
      'captureLifecycleNotResumed',
      details: patchbayLifecycleDetails(_lifecycleState),
    );
  }
  final _CaptureResolution after = _resolve(request);
  if (!after.resolved) return _rejectResolution(requestId, after);
  if (!identical(before.boundary, after.boundary)) {
    return _reject(requestId, 'captureTargetChanged');
  }

  final RenderRepaintBoundary boundary = after.boundary!;
  if (needsPaintForMode(boundary, isDebugBuild: kDebugMode) ||
      !boundary.hasSize ||
      boundary.size.isEmpty) {
    return _reject(requestId, 'captureTargetNotPainted');
  }
  final int expectedWidth = (boundary.size.width * pixelRatio).ceil();
  final int expectedHeight = (boundary.size.height * pixelRatio).ceil();
  if (expectedWidth <= 0 ||
      expectedHeight <= 0 ||
      expectedWidth * expectedHeight > maxPixels) {
    return _reject(
      requestId,
      'capturePixelLimitExceeded',
      details: <String, Object?>{
        'width': expectedWidth,
        'height': expectedHeight,
        'maxPixels': maxPixels,
      },
    );
  }

  final Duration encodingRemaining = deadline.difference(DateTime.now());
  if (encodingRemaining <= Duration.zero) {
    return _reject(requestId, 'captureEncodingTimeout');
  }
  final PatchbayEncodedCapture encoded;
  try {
    encoded = await _encoder(boundary, pixelRatio).timeout(encodingRemaining);
  } on TimeoutException {
    return _reject(requestId, 'captureEncodingTimeout');
  } on Object {
    return _reject(requestId, 'captureEncodingFailed');
  }
  if (!_isPng(encoded.bytes) ||
      encoded.width <= 0 ||
      encoded.height <= 0 ||
      encoded.width * encoded.height > maxPixels) {
    return _reject(requestId, 'captureEncodingFailed');
  }
  if (encoded.bytes.length > maxBytes) {
    return _reject(
      requestId,
      'captureByteLimitExceeded',
      details: <String, Object?>{
        'length': encoded.bytes.length,
        'maxBytes': maxBytes,
      },
    );
  }

  final PatchbayBlobMetadataWire blob;
  try {
    blob = _artifacts.blobs.put(
      encoded.bytes,
      kind: PatchbayBlobSourceWire.flutterCapture,
      source: PatchbayFactSourceWire.uiObserved,
      contentType: 'image/png',
      filename: request.targetId == null
          ? 'patchbay-root.png'
          : 'patchbay-${request.targetId}.png',
      properties: <String, Object?>{
        'width': encoded.width,
        'height': encoded.height,
        'pixelRatio': pixelRatio,
        'pixelFormat': 'rgba8888',
      },
    );
  } on PatchbayBlobFailure catch (failure) {
    return _reject(requestId, switch (failure.code) {
      PatchbayBlobFailureCode.capacityExceeded => 'blobCapacityExceeded',
      PatchbayBlobFailureCode.blobTooLarge => 'blobTooLarge',
      _ => 'blobAdmissionFailed',
    }, details: failure.details);
  }
  return PatchbayInvocation.accepted(
    requestId: requestId,
    payload: PatchbayCaptureResultWire(
      outcome: 'captured',
      source: PatchbayFactSourceWire.uiObserved,
      target: request.targetId == null
          ? PatchbayCaptureTargetWire.root
          : PatchbayCaptureTargetWire.registeredTarget,
      targetId: request.targetId,
      generation: request.generation,
      width: encoded.width,
      height: encoded.height,
      pixelRatio: pixelRatio,
      pixelFormat: 'rgba8888',
      capturedAt: blob.createdAt,
      requestedFrames: requestedFrames,
      observedFrames: observedFrames,
      maxPixels: maxPixels,
      maxBytes: maxBytes,
      warnings: const <PatchbayCaptureWarningWire>[
        PatchbayCaptureWarningWire.flutterSubtreeOnly,
        PatchbayCaptureWarningWire.platformViewsMayBeMissing,
        PatchbayCaptureWarningWire.systemUiNotIncluded,
      ],
      blob: blob,
    ).toJson(),
  );
}