capture method
Future<PatchbayInvocation>
capture(
- PatchbayCaptureRequestWire request, {
- 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(),
);
}