From e8a9bbc19df60e95466facb674a235b6f21cc508 Mon Sep 17 00:00:00 2001 From: protheeuz Date: Thu, 16 Oct 2025 10:52:39 +0700 Subject: [PATCH] [sdk] Add MapRecorder support Implements MapRecorder functionality to achieve feature parity with Android and iOS native SDKs. - Add Pigeon-generated platform communication layer - Implement MapRecorderController for Android and iOS platforms - Create MapRecorder wrapper class with user-facing API - Integrate into MapboxMap via recorder property - Add working example demonstrating record/replay/pause features - Mark API as @experimental matching native SDK status --- .../maps/mapbox_maps/MapRecorderController.kt | 88 +++++ .../maps/mapbox_maps/MapboxMapController.kt | 4 + .../pigeons/MapRecorderMessenger.kt | 303 +++++++++++++++++ example/lib/main.dart | 2 + example/lib/map_recorder_example.dart | 271 +++++++++++++++ .../Generated/MapRecorderMessenger.swift | 289 ++++++++++++++++ .../Classes/MapRecorderController.swift | 76 +++++ .../Classes/MapboxMapController.swift | 7 + lib/mapbox_maps_flutter.dart | 2 + lib/src/map_recorder.dart | 106 ++++++ lib/src/mapbox_map.dart | 35 ++ lib/src/pigeons/map_recorder_messenger.dart | 321 ++++++++++++++++++ 12 files changed, 1504 insertions(+) create mode 100644 android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapRecorderController.kt create mode 100644 android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/MapRecorderMessenger.kt create mode 100644 example/lib/map_recorder_example.dart create mode 100644 ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/Generated/MapRecorderMessenger.swift create mode 100644 ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapRecorderController.swift create mode 100644 lib/src/map_recorder.dart create mode 100644 lib/src/pigeons/map_recorder_messenger.dart diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapRecorderController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapRecorderController.kt new file mode 100644 index 000000000..45ac0c3fc --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapRecorderController.kt @@ -0,0 +1,88 @@ +package com.mapbox.maps.mapbox_maps + +import com.mapbox.bindgen.DataRef +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.MapboxMapRecorder +import com.mapbox.maps.mapbox_maps.pigeons.MapPlayerOptions +import com.mapbox.maps.mapbox_maps.pigeons.MapRecorderOptions +import com.mapbox.maps.mapbox_maps.pigeons._MapRecorderMessenger +import java.nio.ByteBuffer + +/** + * Controller for MapRecorder functionality. + * + * Provides functions to record and replay API calls of a MapboxMap instance. + * These recordings can be used to debug issues which require multiple steps to reproduce. + * Additionally, playbacks can be used for performance testing custom scenarios. + */ +@OptIn(MapboxExperimental::class) +class MapRecorderController( + private val mapboxMap: MapboxMap +) : _MapRecorderMessenger { + + private var recorder: MapboxMapRecorder? = null + + /** + * Get or create the recorder instance. + */ + private fun getRecorder(): MapboxMapRecorder { + if (recorder == null) { + recorder = mapboxMap.createRecorder() + } + return recorder!! + } + + override fun startRecording(options: MapRecorderOptions) { + val nativeOptions = com.mapbox.maps.MapRecorderOptions.Builder() + .apply { + options.timeWindow?.let { timeWindow(it.toLong()) } + loggingEnabled(options.loggingEnabled) + compressed(options.compressed) + } + .build() + + getRecorder().startRecording(nativeOptions) + } + + override fun stopRecording(callback: (Result) -> Unit) { + try { + val data = getRecorder().stopRecording() + val bytes = ByteArray(data.remaining()) + data.get(bytes) + data.rewind() + callback(Result.success(bytes)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun replay( + recordedSequence: ByteArray, + options: MapPlayerOptions, + callback: (Result) -> Unit + ) { + try { + val nativeOptions = com.mapbox.maps.MapPlayerOptions.Builder() + .playbackCount(options.playbackCount.toInt()) + .playbackSpeedMultiplier(options.playbackSpeedMultiplier) + .avoidPlaybackPauses(options.avoidPlaybackPauses) + .build() + + val buffer = ByteBuffer.wrap(recordedSequence) + getRecorder().replay(buffer, nativeOptions) { + callback(Result.success(Unit)) + } + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun togglePauseReplay() { + getRecorder().togglePauseReplay() + } + + override fun getPlaybackState(): String { + return getRecorder().getPlaybackState() + } +} diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt index b9ed59443..d53428703 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt @@ -33,6 +33,7 @@ import com.mapbox.maps.mapbox_maps.pigeons._LocationComponentSettingsInterface import com.mapbox.maps.mapbox_maps.pigeons._MapInterface import com.mapbox.maps.mapbox_maps.pigeons._PerformanceStatisticsApi import com.mapbox.maps.mapbox_maps.pigeons._ViewportMessenger +import com.mapbox.maps.mapbox_maps.pigeons._MapRecorderMessenger import com.mapbox.maps.plugin.animation.camera import com.mapbox.maps.plugin.viewport.viewport import io.flutter.embedding.android.FlutterActivity @@ -122,6 +123,7 @@ class MapboxMapController( private val compassController: CompassController private val viewportController: ViewportController private val performanceStatisticsController: PerformanceStatisticsController + private val mapRecorderController: MapRecorderController private val eventHandler: MapboxEventHandler @@ -206,6 +208,7 @@ class MapboxMapController( compassController = CompassController(mapView) viewportController = ViewportController(mapView.viewport, mapView.camera, context, mapboxMap) performanceStatisticsController = PerformanceStatisticsController(mapboxMap, this.messenger, this.channelSuffix) + mapRecorderController = MapRecorderController(mapboxMap) changeUserAgent(pluginVersion) StyleManager.setUp(messenger, styleController, this.channelSuffix) @@ -222,6 +225,7 @@ class MapboxMapController( CompassSettingsInterface.setUp(messenger, compassController, this.channelSuffix) _ViewportMessenger.setUp(messenger, viewportController, this.channelSuffix) _PerformanceStatisticsApi.setUp(messenger, performanceStatisticsController, this.channelSuffix) + _MapRecorderMessenger.setUp(messenger, mapRecorderController, this.channelSuffix) methodChannel = MethodChannel(messenger, "plugins.flutter.io.$channelSuffix") methodChannel.setMethodCallHandler(this) diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/MapRecorderMessenger.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/MapRecorderMessenger.kt new file mode 100644 index 000000000..c488156f4 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/MapRecorderMessenger.kt @@ -0,0 +1,303 @@ +// Autogenerated from Pigeon (v25.2.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.mapbox.maps.mapbox_maps.pigeons + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +/** + * Options for recording the map when using MapRecorder. + * + * These recordings can be used to debug issues which require multiple steps to reproduce. + * Additionally, playbacks can be used for performance testing custom scenarios. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class MapRecorderOptions ( + /** + * The maximum duration (in milliseconds) from the current time until API calls are kept. + * If not specified, all API calls will be kept during the recording, + * which can lead to significant memory consumption for long sessions. + */ + val timeWindow: Long? = null, + /** If set to true, the recorded API calls will be printed in the logs. */ + val loggingEnabled: Boolean, + /** If set to true, the recorded output will be compressed with gzip. */ + val compressed: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): MapRecorderOptions { + val timeWindow = pigeonVar_list[0] as Long? + val loggingEnabled = pigeonVar_list[1] as Boolean + val compressed = pigeonVar_list[2] as Boolean + return MapRecorderOptions(timeWindow, loggingEnabled, compressed) + } + } + fun toList(): List { + return listOf( + timeWindow, + loggingEnabled, + compressed, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is MapRecorderOptions) { + return false + } + if (this === other) { + return true + } + return timeWindow == other.timeWindow + && loggingEnabled == other.loggingEnabled + && compressed == other.compressed + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Options for playback when using MapRecorder. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class MapPlayerOptions ( + /** The number of times the sequence is played. If negative, the playback loops indefinitely. */ + val playbackCount: Long, + /** Multiplies the speed of playback for faster or slower replays. (1 means no change.) */ + val playbackSpeedMultiplier: Double, + /** + * When set to true, the player will try to interpolate actions between short wait actions, + * to continuously render during the playback. + * This can help to maintain a consistent load during performance testing. + */ + val avoidPlaybackPauses: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): MapPlayerOptions { + val playbackCount = pigeonVar_list[0] as Long + val playbackSpeedMultiplier = pigeonVar_list[1] as Double + val avoidPlaybackPauses = pigeonVar_list[2] as Boolean + return MapPlayerOptions(playbackCount, playbackSpeedMultiplier, avoidPlaybackPauses) + } + } + fun toList(): List { + return listOf( + playbackCount, + playbackSpeedMultiplier, + avoidPlaybackPauses, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is MapPlayerOptions) { + return false + } + if (this === other) { + return true + } + return playbackCount == other.playbackCount + && playbackSpeedMultiplier == other.playbackSpeedMultiplier + && avoidPlaybackPauses == other.avoidPlaybackPauses + } + + override fun hashCode(): Int = toList().hashCode() +} +private open class MapRecorderMessengerPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + MapRecorderOptions.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + MapPlayerOptions.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is MapRecorderOptions -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is MapPlayerOptions -> { + stream.write(130) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** + * Interface for MapRecorder functionality. + * + * MapRecorder provides functions to record and replay API calls of a MapboxMap instance. + * These recordings can be used to debug issues which require multiple steps to reproduce. + * Additionally, playbacks can be used for performance testing custom scenarios. + * + * Note: The raw format produced by stopRecording is experimental and there is no guarantee + * for version cross-compatibility when feeding it to replay. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface _MapRecorderMessenger { + /** + * Begins the recording session. + * + * @param options MapRecorderOptions to control recording. + */ + fun startRecording(options: MapRecorderOptions) + /** + * Stops the current recording session. + * Recorded section could be replayed with replay function. + * + * @return the Uint8List containing the recorded sequence in raw format. + */ + fun stopRecording(callback: (Result) -> Unit) + /** + * Replay a supplied sequence. + * + * @param recordedSequence Sequence recorded with stopRecording method. + * @param options Options to customize the behaviour of the playback. + */ + fun replay(recordedSequence: ByteArray, options: MapPlayerOptions, callback: (Result) -> Unit) + /** Temporarily pauses or resumes playback if already paused. */ + fun togglePauseReplay() + /** Returns the string description of the current state of playback. */ + fun getPlaybackState(): String + + companion object { + /** The codec used by _MapRecorderMessenger. */ + val codec: MessageCodec by lazy { + MapRecorderMessengerPigeonCodec() + } + /** Sets up an instance of `_MapRecorderMessenger` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: _MapRecorderMessenger?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.startRecording$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val optionsArg = args[0] as MapRecorderOptions + val wrapped: List = try { + api.startRecording(optionsArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.stopRecording$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.stopRecording{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.replay$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val recordedSequenceArg = args[0] as ByteArray + val optionsArg = args[1] as MapPlayerOptions + api.replay(recordedSequenceArg, optionsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.togglePauseReplay$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.togglePauseReplay() + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.getPlaybackState$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getPlaybackState()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index bc8ecf180..3c12b4844 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,6 +36,7 @@ import 'projection_example.dart'; import 'style_example.dart'; import 'gestures_example.dart'; import 'debug_options_example.dart'; +import 'map_recorder_example.dart'; final List _allPages = [ SimpleMapExample(), @@ -56,6 +57,7 @@ final List _allPages = [ MapInterfaceExample(), StyleClustersExample(), AnimationExample(), + MapRecorderExample(), PointAnnotationExample(), CircleAnnotationExample(), PolylineAnnotationExample(), diff --git a/example/lib/map_recorder_example.dart b/example/lib/map_recorder_example.dart new file mode 100644 index 000000000..7987f9551 --- /dev/null +++ b/example/lib/map_recorder_example.dart @@ -0,0 +1,271 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; +import 'example.dart'; + +/// Example demonstrating the MapRecorder functionality. +/// +/// MapRecorder allows you to record and replay map interactions, +/// which is useful for debugging and performance testing. +class MapRecorderExample extends StatefulWidget implements Example { + @override + final Widget leading = const Icon(Icons.fiber_smart_record); + @override + final String title = 'Map Recorder'; + @override + final String? subtitle = 'Record and replay map sessions'; + + @override + State createState() => MapRecorderExampleState(); +} + +class MapRecorderExampleState extends State { + MapboxMap? mapboxMap; + Uint8List? recordedSequence; + bool isRecording = false; + bool isReplaying = false; + String playbackState = 'IDLE'; + + _onMapCreated(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + } + + Future _startRecording() async { + if (mapboxMap == null) return; + + setState(() { + isRecording = true; + recordedSequence = null; + }); + + try { + await mapboxMap!.recorder.startRecording( + timeWindow: 60000, // Keep last 60 seconds + loggingEnabled: true, + compressed: true, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Recording started. Interact with the map!'), + duration: Duration(seconds: 2), + ), + ); + + // Perform some camera animations to record + await Future.delayed(const Duration(milliseconds: 500)); + await mapboxMap!.flyTo( + CameraOptions( + center: Point(coordinates: Position(-73.581, 45.4588)), + zoom: 11.0, + pitch: 35.0, + bearing: 90.0, + ), + MapAnimationOptions(duration: 5000, startDelay: 0), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error starting recording: $e')), + ); + setState(() { + isRecording = false; + }); + } + } + + Future _stopRecording() async { + if (mapboxMap == null) return; + + try { + final sequence = await mapboxMap!.recorder.stopRecording(); + setState(() { + isRecording = false; + recordedSequence = sequence; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Recording stopped. ${sequence.length} bytes recorded.'), + duration: const Duration(seconds: 2), + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error stopping recording: $e')), + ); + setState(() { + isRecording = false; + }); + } + } + + Future _replayRecording() async { + if (mapboxMap == null || recordedSequence == null) return; + + setState(() { + isReplaying = true; + }); + + try { + // Replay twice at 2x speed + await mapboxMap!.recorder.replay( + recordedSequence!, + playbackCount: 2, + playbackSpeedMultiplier: 2.0, + avoidPlaybackPauses: false, + ); + + setState(() { + isReplaying = false; + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Replay completed!'), + duration: Duration(seconds: 2), + ), + ); + + // Get final playback state + final state = await mapboxMap!.recorder.getState(); + setState(() { + playbackState = state; + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error during replay: $e')), + ); + setState(() { + isReplaying = false; + }); + } + } + + Future _togglePause() async { + if (mapboxMap == null) return; + + try { + await mapboxMap!.recorder.togglePause(); + final state = await mapboxMap!.recorder.getState(); + setState(() { + playbackState = state; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Playback state: $state'), + duration: const Duration(seconds: 1), + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error toggling pause: $e')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MapWidget( + key: const ValueKey("mapWidget"), + cameraOptions: CameraOptions( + center: Point(coordinates: Position(-74.0060, 40.7128)), + zoom: 10.0, + ), + styleUri: MapboxStyles.STANDARD, + onMapCreated: _onMapCreated, + ), + ), + Container( + padding: const EdgeInsets.all(16), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Status: ${isRecording ? "Recording..." : isReplaying ? "Replaying..." : "Idle"}', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Playback State: $playbackState', + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: isRecording || isReplaying + ? null + : _startRecording, + icon: const Icon(Icons.fiber_manual_record), + label: const Text('Record'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: + isRecording && !isReplaying ? _stopRecording : null, + icon: const Icon(Icons.stop), + label: const Text('Stop'), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: recordedSequence != null && + !isRecording && + !isReplaying + ? _replayRecording + : null, + icon: const Icon(Icons.play_arrow), + label: const Text('Replay 2x'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: isReplaying ? _togglePause : null, + icon: const Icon(Icons.pause), + label: const Text('Pause'), + ), + ), + ], + ), + if (recordedSequence != null) ...[ + const SizedBox(height: 8), + Text( + 'Recorded: ${(recordedSequence!.length / 1024).toStringAsFixed(2)} KB', + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/Generated/MapRecorderMessenger.swift b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/Generated/MapRecorderMessenger.swift new file mode 100644 index 000000000..082fae215 --- /dev/null +++ b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/Generated/MapRecorderMessenger.swift @@ -0,0 +1,289 @@ +// Autogenerated from Pigeon (v25.2.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// Options for recording the map when using MapRecorder. +/// +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct MapRecorderOptions { + /// The maximum duration (in milliseconds) from the current time until API calls are kept. + /// If not specified, all API calls will be kept during the recording, + /// which can lead to significant memory consumption for long sessions. + var timeWindow: Int64? = nil + /// If set to true, the recorded API calls will be printed in the logs. + var loggingEnabled: Bool + /// If set to true, the recorded output will be compressed with gzip. + var compressed: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> MapRecorderOptions? { + let timeWindow: Int64? = nilOrValue(pigeonVar_list[0]) + let loggingEnabled = pigeonVar_list[1] as! Bool + let compressed = pigeonVar_list[2] as! Bool + + return MapRecorderOptions( + timeWindow: timeWindow, + loggingEnabled: loggingEnabled, + compressed: compressed + ) + } + func toList() -> [Any?] { + return [ + timeWindow, + loggingEnabled, + compressed, + ] + } +} + +/// Options for playback when using MapRecorder. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct MapPlayerOptions { + /// The number of times the sequence is played. If negative, the playback loops indefinitely. + var playbackCount: Int64 + /// Multiplies the speed of playback for faster or slower replays. (1 means no change.) + var playbackSpeedMultiplier: Double + /// When set to true, the player will try to interpolate actions between short wait actions, + /// to continuously render during the playback. + /// This can help to maintain a consistent load during performance testing. + var avoidPlaybackPauses: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> MapPlayerOptions? { + let playbackCount = pigeonVar_list[0] as! Int64 + let playbackSpeedMultiplier = pigeonVar_list[1] as! Double + let avoidPlaybackPauses = pigeonVar_list[2] as! Bool + + return MapPlayerOptions( + playbackCount: playbackCount, + playbackSpeedMultiplier: playbackSpeedMultiplier, + avoidPlaybackPauses: avoidPlaybackPauses + ) + } + func toList() -> [Any?] { + return [ + playbackCount, + playbackSpeedMultiplier, + avoidPlaybackPauses, + ] + } +} + +private class MapRecorderMessengerPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return MapRecorderOptions.fromList(self.readValue() as! [Any?]) + case 130: + return MapPlayerOptions.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MapRecorderMessengerPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? MapRecorderOptions { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? MapPlayerOptions { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MapRecorderMessengerPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MapRecorderMessengerPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MapRecorderMessengerPigeonCodecWriter(data: data) + } +} + +class MapRecorderMessengerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MapRecorderMessengerPigeonCodec(readerWriter: MapRecorderMessengerPigeonCodecReaderWriter()) +} + + +/// Interface for MapRecorder functionality. +/// +/// MapRecorder provides functions to record and replay API calls of a MapboxMap instance. +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +/// +/// Note: The raw format produced by stopRecording is experimental and there is no guarantee +/// for version cross-compatibility when feeding it to replay. +/// +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol _MapRecorderMessenger { + /// Begins the recording session. + /// + /// @param options MapRecorderOptions to control recording. + func startRecording(options: MapRecorderOptions) throws + /// Stops the current recording session. + /// Recorded section could be replayed with replay function. + /// + /// @return the Uint8List containing the recorded sequence in raw format. + func stopRecording(completion: @escaping (Result) -> Void) + /// Replay a supplied sequence. + /// + /// @param recordedSequence Sequence recorded with stopRecording method. + /// @param options Options to customize the behaviour of the playback. + func replay(recordedSequence: FlutterStandardTypedData, options: MapPlayerOptions, completion: @escaping (Result) -> Void) + /// Temporarily pauses or resumes playback if already paused. + func togglePauseReplay() throws + /// Returns the string description of the current state of playback. + func getPlaybackState() throws -> String +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class _MapRecorderMessengerSetup { + static var codec: FlutterStandardMessageCodec { MapRecorderMessengerPigeonCodec.shared } + /// Sets up an instance of `_MapRecorderMessenger` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: _MapRecorderMessenger?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Begins the recording session. + /// + /// @param options MapRecorderOptions to control recording. + let startRecordingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.startRecording\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + startRecordingChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! MapRecorderOptions + do { + try api.startRecording(options: optionsArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + startRecordingChannel.setMessageHandler(nil) + } + /// Stops the current recording session. + /// Recorded section could be replayed with replay function. + /// + /// @return the Uint8List containing the recorded sequence in raw format. + let stopRecordingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.stopRecording\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopRecordingChannel.setMessageHandler { _, reply in + api.stopRecording { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopRecordingChannel.setMessageHandler(nil) + } + /// Replay a supplied sequence. + /// + /// @param recordedSequence Sequence recorded with stopRecording method. + /// @param options Options to customize the behaviour of the playback. + let replayChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.replay\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + replayChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let recordedSequenceArg = args[0] as! FlutterStandardTypedData + let optionsArg = args[1] as! MapPlayerOptions + api.replay(recordedSequence: recordedSequenceArg, options: optionsArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + replayChannel.setMessageHandler(nil) + } + /// Temporarily pauses or resumes playback if already paused. + let togglePauseReplayChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.togglePauseReplay\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + togglePauseReplayChannel.setMessageHandler { _, reply in + do { + try api.togglePauseReplay() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + togglePauseReplayChannel.setMessageHandler(nil) + } + /// Returns the string description of the current state of playback. + let getPlaybackStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.getPlaybackState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getPlaybackStateChannel.setMessageHandler { _, reply in + do { + let result = try api.getPlaybackState() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getPlaybackStateChannel.setMessageHandler(nil) + } + } +} diff --git a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapRecorderController.swift b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapRecorderController.swift new file mode 100644 index 000000000..9fa2bbc64 --- /dev/null +++ b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapRecorderController.swift @@ -0,0 +1,76 @@ +import Foundation +@_spi(Experimental) import MapboxMaps +import Flutter + +/// Controller for MapRecorder functionality. +/// +/// Provides functions to record and replay API calls of a MapboxMap instance. +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +class MapRecorderController: _MapRecorderMessenger { + private let mapboxMap: MapboxMap + private var recorder: MapRecorder? + + init(mapboxMap: MapboxMap) { + self.mapboxMap = mapboxMap + } + + /// Get or create the recorder instance. + private func getRecorder() throws -> MapRecorder { + if recorder == nil { + recorder = try mapboxMap.makeRecorder() + } + return recorder! + } + + func startRecording(options: MapRecorderOptions) throws { + let nativeOptions = MapboxMaps.MapRecorderOptions( + timeWindow: options.timeWindow.map { Int($0) }, + loggingEnabled: options.loggingEnabled, + compressed: options.compressed + ) + + try getRecorder().start(options: nativeOptions) + } + + func stopRecording(completion: @escaping (Result) -> Void) { + do { + let data = try getRecorder().stop() + let typedData = FlutterStandardTypedData(bytes: data) + completion(.success(typedData)) + } catch { + completion(.failure(error)) + } + } + + func replay( + recordedSequence: FlutterStandardTypedData, + options: MapPlayerOptions, + completion: @escaping (Result) -> Void + ) { + do { + let nativeOptions = MapboxMaps.MapPlayerOptions( + playbackCount: Int(options.playbackCount), + playbackSpeedMultiplier: options.playbackSpeedMultiplier, + avoidPlaybackPauses: options.avoidPlaybackPauses + ) + + try getRecorder().replay( + recordedSequence: recordedSequence.data, + options: nativeOptions + ) { + completion(.success(())) + } + } catch { + completion(.failure(error)) + } + } + + func togglePauseReplay() throws { + try getRecorder().togglePauseReplay() + } + + func getPlaybackState() throws -> String { + return try getRecorder().playbackState() + } +} diff --git a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift index 729f18e21..0ed9d70b9 100644 --- a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift +++ b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift @@ -102,6 +102,13 @@ final class MapboxMapController: NSObject, FlutterPlatformView { messageChannelSuffix: binaryMessenger.suffix ) + let mapRecorderController = MapRecorderController(mapboxMap: mapView.mapboxMap) + _MapRecorderMessengerSetup.setUp( + binaryMessenger: binaryMessenger.messenger, + api: mapRecorderController, + messageChannelSuffix: binaryMessenger.suffix + ) + super.init() channel.setMethodCallHandler { [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } diff --git a/lib/mapbox_maps_flutter.dart b/lib/mapbox_maps_flutter.dart index c9b4be0a4..a9d10580f 100644 --- a/lib/mapbox_maps_flutter.dart +++ b/lib/mapbox_maps_flutter.dart @@ -24,6 +24,7 @@ part 'src/events.dart'; part 'src/map_widget.dart'; part 'src/mapbox_map.dart'; part 'src/mapbox_maps_options.dart'; +part 'src/map_recorder.dart'; part 'src/mapbox_maps_platform.dart'; part 'src/pigeons/circle_annotation_messenger.dart'; part 'src/pigeons/point_annotation_messenger.dart'; @@ -32,6 +33,7 @@ part 'src/pigeons/polyline_annotation_messenger.dart'; part 'src/pigeons/map_interfaces.dart'; part 'src/pigeons/settings.dart'; part 'src/pigeons/gesture_listeners.dart'; +part 'src/pigeons/map_recorder_messenger.dart'; part 'src/snapshotter/snapshotter_messenger.dart'; part 'src/pigeons/log_backend.dart'; part 'src/pigeons/performace_statistics.dart'; diff --git a/lib/src/map_recorder.dart b/lib/src/map_recorder.dart new file mode 100644 index 000000000..33931b39c --- /dev/null +++ b/lib/src/map_recorder.dart @@ -0,0 +1,106 @@ +part of mapbox_maps_flutter; + +/// MapRecorder provides functions to record and replay API calls of a [MapboxMap] instance. +/// +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +/// +/// **Note**: The raw format produced by [stopRecording] is experimental and there is no guarantee +/// for version cross-compatibility when feeding it to [replay]. +/// +/// **Warning**: This API is experimental and may change in future releases. +/// +/// **Example**: +/// ```dart +/// // Start recording +/// await mapboxMap.recorder.startRecording( +/// timeWindow: 60000, // 60 seconds +/// loggingEnabled: true, +/// compressed: true, +/// ); +/// +/// // ... perform some map interactions ... +/// +/// // Stop recording and get the recorded sequence +/// final recordedSequence = await mapboxMap.recorder.stopRecording(); +/// +/// // Replay the sequence twice at 2x speed +/// await mapboxMap.recorder.replay( +/// recordedSequence, +/// playbackCount: 2, +/// playbackSpeedMultiplier: 2.0, +/// avoidPlaybackPauses: false, +/// ); +/// ``` +@experimental +class MapRecorder { + final _MapRecorderMessenger _messenger; + + MapRecorder._(this._messenger); + + /// Begins the recording session. + /// + /// [timeWindow] - The maximum duration (in milliseconds) from the current time until API calls are kept. + /// If not specified, all API calls will be kept during the recording, which can lead to significant memory consumption for long sessions. + /// + /// [loggingEnabled] - If set to true, the recorded API calls will be printed in the logs. Default value: false. + /// + /// [compressed] - If set to true, the recorded output will be compressed with gzip. Default value: false. + Future startRecording({ + int? timeWindow, + bool loggingEnabled = false, + bool compressed = false, + }) { + return _messenger.startRecording( + MapRecorderOptions( + timeWindow: timeWindow, + loggingEnabled: loggingEnabled, + compressed: compressed, + ), + ); + } + + /// Stops the current recording session. + /// + /// Recorded section can be replayed with [replay] method. + /// Returns a [Uint8List] containing the recorded sequence in raw format. + Future stopRecording() { + return _messenger.stopRecording(); + } + + /// Replay a supplied sequence. + /// + /// [recordedSequence] - Sequence recorded with [stopRecording] method. + /// + /// [playbackCount] - The number of times the sequence is played. If negative, the playback loops indefinitely. Default value: 1. + /// + /// [playbackSpeedMultiplier] - Multiplies the speed of playback for faster or slower replays. 1.0 means no change. Default value: 1.0. + /// + /// [avoidPlaybackPauses] - When set to true, the player will try to interpolate actions between short wait actions, + /// to continuously render during the playback. This can help to maintain a consistent load during performance testing. Default value: false. + Future replay( + Uint8List recordedSequence, { + int playbackCount = 1, + double playbackSpeedMultiplier = 1.0, + bool avoidPlaybackPauses = false, + }) { + return _messenger.replay( + recordedSequence, + MapPlayerOptions( + playbackCount: playbackCount, + playbackSpeedMultiplier: playbackSpeedMultiplier, + avoidPlaybackPauses: avoidPlaybackPauses, + ), + ); + } + + /// Temporarily pauses or resumes playback if already paused. + Future togglePause() { + return _messenger.togglePauseReplay(); + } + + /// Returns the string description of the current state of playback (e.g., "IDLE", "PLAYING", "PAUSED"). + Future getState() { + return _messenger.getPlaybackState(); + } +} diff --git a/lib/src/mapbox_map.dart b/lib/src/mapbox_map.dart index 1ca57a7dd..9b0808dbf 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -177,10 +177,45 @@ class MapboxMap extends ChangeNotifier { _PerformanceStatisticsApi( binaryMessenger: _mapboxMapsPlatform.binaryMessenger, messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + late final _MapRecorderMessenger _mapRecorderMessenger = + _MapRecorderMessenger( + binaryMessenger: _mapboxMapsPlatform.binaryMessenger, + messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); /// The interface to create and set annotations. late final AnnotationManager annotations; + /// The interface to record and replay map sessions. + /// + /// MapRecorder provides functions to record and replay API calls of a MapboxMap instance. + /// These recordings can be used to debug issues which require multiple steps to reproduce. + /// Additionally, playbacks can be used for performance testing custom scenarios. + /// + /// **Warning**: This API is experimental and may change in future releases. + /// + /// **Example**: + /// ```dart + /// // Start recording + /// await mapboxMap.recorder.startRecording( + /// timeWindow: 60000, + /// loggingEnabled: true, + /// ); + /// + /// // ... perform map interactions ... + /// + /// // Stop and get recorded sequence + /// final sequence = await mapboxMap.recorder.stopRecording(); + /// + /// // Replay at 2x speed + /// await mapboxMap.recorder.replay( + /// sequence, + /// playbackCount: 1, + /// playbackSpeedMultiplier: 2.0, + /// ); + /// ``` + @experimental + late final MapRecorder recorder = MapRecorder._(_mapRecorderMessenger); + // Keep Projection visible for users as iOS doesn't include it in MapboxMaps. /// The map projection of the style. late final Projection projection = Projection( diff --git a/lib/src/pigeons/map_recorder_messenger.dart b/lib/src/pigeons/map_recorder_messenger.dart new file mode 100644 index 000000000..427ceb42a --- /dev/null +++ b/lib/src/pigeons/map_recorder_messenger.dart @@ -0,0 +1,321 @@ +// Autogenerated from Pigeon (v25.2.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +part of mapbox_maps_flutter; + + +/// Options for recording the map when using MapRecorder. +/// +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +class MapRecorderOptions { + MapRecorderOptions({ + this.timeWindow, + required this.loggingEnabled, + required this.compressed, + }); + + /// The maximum duration (in milliseconds) from the current time until API calls are kept. + /// If not specified, all API calls will be kept during the recording, + /// which can lead to significant memory consumption for long sessions. + int? timeWindow; + + /// If set to true, the recorded API calls will be printed in the logs. + bool loggingEnabled; + + /// If set to true, the recorded output will be compressed with gzip. + bool compressed; + + List _toList() { + return [ + timeWindow, + loggingEnabled, + compressed, + ]; + } + + Object encode() { + return _toList(); } + + static MapRecorderOptions decode(Object result) { + result as List; + return MapRecorderOptions( + timeWindow: result[0] as int?, + loggingEnabled: result[1]! as bool, + compressed: result[2]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! MapRecorderOptions || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return + timeWindow == other.timeWindow + && loggingEnabled == other.loggingEnabled + && compressed == other.compressed; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Options for playback when using MapRecorder. +class MapPlayerOptions { + MapPlayerOptions({ + required this.playbackCount, + required this.playbackSpeedMultiplier, + required this.avoidPlaybackPauses, + }); + + /// The number of times the sequence is played. If negative, the playback loops indefinitely. + int playbackCount; + + /// Multiplies the speed of playback for faster or slower replays. (1 means no change.) + double playbackSpeedMultiplier; + + /// When set to true, the player will try to interpolate actions between short wait actions, + /// to continuously render during the playback. + /// This can help to maintain a consistent load during performance testing. + bool avoidPlaybackPauses; + + List _toList() { + return [ + playbackCount, + playbackSpeedMultiplier, + avoidPlaybackPauses, + ]; + } + + Object encode() { + return _toList(); } + + static MapPlayerOptions decode(Object result) { + result as List; + return MapPlayerOptions( + playbackCount: result[0]! as int, + playbackSpeedMultiplier: result[1]! as double, + avoidPlaybackPauses: result[2]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! MapPlayerOptions || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return + playbackCount == other.playbackCount + && playbackSpeedMultiplier == other.playbackSpeedMultiplier + && avoidPlaybackPauses == other.avoidPlaybackPauses; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is MapRecorderOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MapPlayerOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return MapRecorderOptions.decode(readValue(buffer)!); + case 130: + return MapPlayerOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Interface for MapRecorder functionality. +/// +/// MapRecorder provides functions to record and replay API calls of a MapboxMap instance. +/// These recordings can be used to debug issues which require multiple steps to reproduce. +/// Additionally, playbacks can be used for performance testing custom scenarios. +/// +/// Note: The raw format produced by stopRecording is experimental and there is no guarantee +/// for version cross-compatibility when feeding it to replay. +class _MapRecorderMessenger { + /// Constructor for [_MapRecorderMessenger]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + _MapRecorderMessenger({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Begins the recording session. + /// + /// @param options MapRecorderOptions to control recording. + Future startRecording(MapRecorderOptions options) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.startRecording$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Stops the current recording session. + /// Recorded section could be replayed with replay function. + /// + /// @return the Uint8List containing the recorded sequence in raw format. + Future stopRecording() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.stopRecording$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Uint8List?)!; + } + } + + /// Replay a supplied sequence. + /// + /// @param recordedSequence Sequence recorded with stopRecording method. + /// @param options Options to customize the behaviour of the playback. + Future replay(Uint8List recordedSequence, MapPlayerOptions options) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.replay$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([recordedSequence, options]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Temporarily pauses or resumes playback if already paused. + Future togglePauseReplay() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.togglePauseReplay$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Returns the string description of the current state of playback. + Future getPlaybackState() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.mapbox_maps_flutter._MapRecorderMessenger.getPlaybackState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } +}