[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CSNVPAH6VKNBZ3HGSB484F":3,"skill-same:01M6CSNVPAH6VKNBZ3HGSB484F":118},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":6,"ownerHtmlUrl":49,"ownerBlogUrl":50,"ownerDescription":51,"updatedAt":52,"install":53,"resources":66},"01M6CSNVPAH6VKNBZ3HGSB484F","moq-kit",null,"Software Mansion's moq-kit — native Swift (iOS) and Kotlin (Android) SDKs for Media over QUIC (moq-lite) live streaming: sub-second-latency playback, camera\u002Fmicrophone\u002Fscreen publishing, and realtime data tracks over a MoQ relay. MUST USE before writing, reviewing, or debugging ANY code that imports MoQKit (Swift) or com.swmansion.moqkit (Kotlin); for React Native apps use the react-native-moq skill instead. Trigger on: 'moq-kit', 'MoQKit', 'Media over QUIC', 'moq-lite', 'MoQ relay', 'moq-ffi', 'BroadcastSubscription', 'DataTrackEmitter', 'CameraCapture', 'MicrophoneCapture', 'MultiCameraCapture', 'ScreenCapture', 'TrackSubscription', 'AudioDataStream', 'MoQReplayKitBroadcastSampleHandler', or a Session\u002FPlayer\u002FPublisher\u002FCatalog from moq-kit.","software-mansion-labs\u002Fmoq-kit","software-mansion-labs","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6952717?v=4","01M6CSNGCFSFMP7Q0KSAP90W0K","Software Mansion Labs","01M6CSNGGMSH71BFZ21D393C4H",32,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"moq kit","data","react-native","publishing","use","react native","catalog","session","kotlin","Media","writing","ios","ffi","streaming","quic","code","debugging","realtime","react","android","2026-11-10 10:12:30",false,"---\nname: moq-kit\ndescription: \"Software Mansion's moq-kit — native Swift (iOS) and Kotlin (Android) SDKs for Media over QUIC (moq-lite) live streaming: sub-second-latency playback, camera\u002Fmicrophone\u002Fscreen publishing, and realtime data tracks over a MoQ relay. MUST USE before writing, reviewing, or debugging ANY code that imports MoQKit (Swift) or com.swmansion.moqkit (Kotlin); for React Native apps use the react-native-moq skill instead. Trigger on: 'moq-kit', 'MoQKit', 'Media over QUIC', 'moq-lite', 'MoQ relay', 'moq-ffi', 'BroadcastSubscription', 'DataTrackEmitter', 'CameraCapture', 'MicrophoneCapture', 'MultiCameraCapture', 'ScreenCapture', 'TrackSubscription', 'AudioDataStream', 'MoQReplayKitBroadcastSampleHandler', or a Session\u002FPlayer\u002FPublisher\u002FCatalog from moq-kit.\"\nlicense: Apache-2.0\n---\n\n# moq-kit\n\nNative Swift and Kotlin SDKs for Media over QUIC live streaming: connect to a relay, discover broadcasts, play catalog-described streams with sub-second latency, publish camera\u002Fmicrophone\u002Fscreen tracks, and exchange raw data tracks. Wraps the UniFFI bindings generated from `moq-ffi` (Luke Curley's `moq-dev\u002Fmoq`); targets the `moq-lite` protocol, not the IETF `moq-transport` draft.\n\n**Requirements:** iOS 16+ \u002F macOS 13+ (Xcode 16+ to build) \u002F Android minSdk 29, compileSdk 35. A moq-lite relay to connect to. The references cover iOS and Android; macOS is declared by the Swift package but untested here.\n\nmoq-kit is an **active preview**: public APIs, packaging, and codec coverage can still change between releases. Pin an exact version and check signatures against the installed artifact rather than assuming a shape.\n\n**Install — iOS (SPM):**\n\n```swift\n.package(url: \"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\", exact: \"\u003Clatest release>\")\n\u002F\u002F product: .product(name: \"MoQKit\", package: \"moq-kit\")\n```\n\n**Install — Android (Maven Central):**\n\n```kotlin\ndependencies { implementation(\"com.swmansion.moqkit:moqkit:\u003Clatest release>\") }\n```\n\nFill in `\u003Clatest release>` from https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Freleases. Both pins are exact on purpose — loosen the SPM one (`.upToNextMinor(from:)`) only if you mean to track preview releases as they land.\n\n## Platform detection\n\nReference files are split per platform — read only the ones for the project at hand:\n\n- `Package.swift`, `*.xcodeproj`, or a `Podfile` → the `*-ios.md` references (Swift).\n- `build.gradle`\u002F`build.gradle.kts` with `com.swmansion.moqkit` → the `*-android.md` references (Kotlin).\n- Cross-platform work (feature parity, bindings on top of both SDKs) → read both; the APIs are deliberately kept equivalent.\n\n## Mental model\n\n- A **`Session`** owns one QUIC connection to a relay and serves both subscribing and publishing. `connect()` once; a session is **one-shot** — after `close()` (or a connection error) create a new `Session`. Swift: `actor`, `connect() async throws`. Kotlin: optionally takes a `parentScope` (defaults to an internal IO scope; pass `viewModelScope`\u002F`lifecycleScope` so cancelling it tears the session down); `connect()` is the only suspend call.\n- **Subscribing:** `session.subscribe(prefix:)` → stream of `Broadcast` → `broadcast.catalogs()` → pick playable tracks → `Player(catalog:videoTrackName:audioTrackName:targetBuffering:)` → `play()`. Rendering is platform-specific: iOS adds `player.videoLayer` (an `AVSampleBufferDisplayLayer`) to the view hierarchy; Android calls `player.setSurface(surface)` with a `Surface` you own.\n- **Publishing:** create capture sources (`CameraCapture`, `MicrophoneCapture`, `MultiCameraCapture`, `ScreenCapture`) (start them explicitly when hardware is wanted) → `Publisher()` + `addVideoTrack`\u002F`addAudioTrack`\u002F`addDataTrack` → `session.publish(path, publisher)` → `publisher.start()`. A publisher is single-use; add all tracks before `start()`.\n- **Data tracks** (`DataTrackEmitter` → `addDataTrack`; consume with `broadcast.subscribeTrack(name:)`) bypass the media catalog — publisher and subscriber agree on the track name out of band.\n- Everything is observable: `session.state`, `publisher.state`\u002F`events`, player events\u002Fstats. Kotlin uses `StateFlow`\u002F`SharedFlow`\u002Fcold `Flow`; Swift uses `AsyncStream` for session\u002Fpublisher state and diagnostics, but callback subscriptions (`subscribeEvents`\u002F`subscribeStats`) for player events\u002Fstats.\n\n## Quick start — watch\n\n**iOS:**\n\n```swift\nlet session = Session(url: \"http:\u002F\u002Flocalhost:4443\u002Fanon\")\ntry await session.connect()\nlet subscription = try await session.subscribe(prefix: \"live\")\nfor await broadcast in subscription.broadcasts {\n    for await catalog in broadcast.catalogs() {\n        let video = catalog.playableVideoTracks.first?.name\n        let audio = catalog.playableAudioTracks.first?.name\n        guard video != nil || audio != nil else { continue }\n        let player = try await MainActor.run {\n            try Player(catalog: catalog, videoTrackName: video, audioTrackName: audio)\n        }\n        try await player.play() \u002F\u002F render via player.videoLayer — see playback-ios.md\n    }\n}\n```\n\n**Android:**\n\n```kotlin\nscope.launch {\n    val session = Session(url = \"http:\u002F\u002Flocalhost:4443\u002Fanon\", parentScope = scope)\n    session.connect()\n    session.subscribe(prefix = \"live\").broadcasts.collect { broadcast ->\n        broadcast.catalogs().collect { catalog ->\n            val video = catalog.playableVideoTracks.firstOrNull()?.name\n            val audio = catalog.playableAudioTracks.firstOrNull()?.name\n            if (video == null && audio == null) return@collect\n            val player = Player(catalog, video, audio, parentScope = scope)\n            player.setSurface(surfaceView.holder.surface)\n            player.play()\n        }\n    }\n}\n```\n\nMinimal on purpose: the nested loops handle one broadcast at a time — spawn a `Task`\u002F`launch` per broadcast in real code — and each catalog update should tear down the previous player before creating the new one (players hold native handles).\n\n## Quick start — go live\n\nSame four steps on both platforms, and the order is load-bearing (rule 3):\n\n**iOS:**\n\n```swift\ntry await camera.start()                                   \u002F\u002F 1. captures — publisher.start() won't do this\nlet publisher = try Publisher()                            \u002F\u002F 2. every track before start()\ntry publisher.addVideoTrack(name: \"camera\", source: camera, config: VideoEncoderConfig())\ntry await session.publish(path: \"live\u002Fios\", publisher: publisher)  \u002F\u002F 3. register\ntry await publisher.start()                                \u002F\u002F 4. go live\n```\n\n**Android:**\n\n```kotlin\ncamera.start(context, lifecycleOwner)\nval publisher = Publisher()\npublisher.addVideoTrack(name = \"camera\", source = camera, config = VideoEncoderConfig())\nsession.publish(path = \"live\u002Fandroid\", publisher = publisher)\npublisher.start()\n```\n\nCaptures, previews, encoder configs, and permissions are in the publishing references.\n\n## Shared concepts (both platforms)\n\n- **Catalogs:** `broadcast.catalogs()` streams catalog updates — each element **replaces** the previous; the stream ending means the broadcast went away (mark it offline). It often ends by **throwing** rather than completing — a producer that disappears surfaces as `MoqException$Mux: json: remote error: code=…` out of `collect`, so handle the exception and the clean completion identically (both demo apps and react-native-moq do). Always select from `playableVideoTracks` \u002F `playableAudioTracks` (device-filtered on Android and for iOS audio; iOS video playability is codec-family recognition only), not the raw lists; per-track `isPlayable`\u002F`unsupportedReason` explain exclusions (they live on the concrete `VideoTrackInfo`\u002F`AudioTrackInfo`, not the base `TrackInfo`). Video codec strings: `\"avc1\"`, `\"hev1\"`, `\"av01\"`; audio: `\"mp4a.40.2\"`\u002F`\"aac\"` (Android-published tracks advertise `\"aac\"`), `\"opus\"`. AV1 is **playback-only** — neither platform publishes it, and Apple-side decode is iPhone 15 Pro-class hardware, not a general capability.\n- **Broadcast paths are prefix-relative:** subscribing with prefix `\"live\"` reports broadcast `live\u002Fgame` as `path: \"game\"` — subscribe with `\"\"` to see full paths.\n- **Three observation channels on `Player`** — don't mix them up: **events** (lifecycle milestones — `player.init`, `playback.start`, `track.ready`, `track.switch`, `track.stall.start`, `rebuffer.end`, `decode.error` — wire values; Kotlin matches the `PlayerEventName` enum; not replayed), **stats** (`PlaybackStats` at ~1 s cadence — latency, fps, bitrates, `StallStats`, `timeToFirst`, buffer depths, drops, switches), and **diagnostics** (typed per-frame `PipelineEvent` — `frameDropped` with `DropStage`\u002F`DropReason`, `stallStarted`, `latencySample`, … — bounded at 256 drop-oldest — Android hands every caller the same shared flow — and never backpressures playback; for logging\u002Ftelemetry only).\n- **Publisher states:** `Idle \u002F Publishing \u002F Stopped \u002F Error(message)`. Session teardown stops its publishers. Audio\u002Fvideo return `PublishedMediaTrack` with requested `isEnabled` and async\u002Fsuspend `setEnabled(bool)`; states are `Idle \u002F Disabled \u002F Starting \u002F Active \u002F Failed(message) \u002F Stopped`. Raw data returns `PublishedTrack` with state and terminal `stop()` only. Publisher, track, and capture cleanup awaits teardown. Empty publishers stay open until explicit stop or session end.\n- **`TrackDelivery`** for raw track subscription: `monotonic` (default) skips late groups — right for newest-value-wins live state; `arrival` delivers every group in order — right for chat and anything lossless.\n\n## Critical rules\n\n1. **Sessions, publishers, and players are single-use.** A `Session` connects once (`alreadyConnected`\u002F`IllegalStateException` on reuse); after `close()` make a new one. A stopped `Publisher` cannot be restarted. Never restart a torn-down `Player` on either platform — create a fresh one.\n2. **Media track names are local labels, catalog names are muxer-generated.** The `name:` passed to `addVideoTrack`\u002F`addAudioTrack` (defaults `\"video\"`\u002F`\"audio\"`) only labels `PublishedTrack` and publisher events; subscribers must discover real track names from `Catalog.videoTracks`\u002F`audioTracks`. **Exception: data tracks** — `addDataTrack(name:)` (default `\"data\"`) IS the name subscribers pass to `subscribeTrack(name:)`.\n3. **`publisher.start()` does not start capture.** Add tracks → `session.publish(path, publisher)` → `publisher.start()`. Camera\u002Fmicrophone may be started before or after registration. Capture stop suspends enabled publication and releases hardware; track `setEnabled(false)` releases only encoding\u002Fpublication. Capture start resumes enabled tracks.\n4. **Gate codecs before use.** `VideoEncoderConfig.supportedCodecs()` \u002F `AudioEncoderConfig.supportedCodecs()` \u002F `.isSupported` on the publish side; `catalog.playableVideoTracks` \u002F `playableAudioTracks` \u002F per-track `isPlayable` on the playback side. Unsupported configs throw (`SessionError.unsupportedCodec` \u002F `UnsupportedCodecException`). H.264 + AAC\u002FOpus are the best-tested paths; default to H.264. Opus wants 48 kHz — on Android keep `MicrophoneCapture(sampleRate = …)` equal to the encoder config; iOS has no mic rate knob and resamples internally.\n5. **Player startup events are not replayed.** Subscribe before `play()`: iOS `player.subscribeEvents { ... }` (and **retain** the returned subscription — it cancels on deinit); Android collect `player.events` with `CoroutineStart.UNDISPATCHED`. On both platforms `player.init` fires during construction and can never be observed — the rule protects `track.ready`\u002F`playback.start`.\n6. **Permissions and audio sessions are the app's job.** iOS: `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, `NSLocalNetworkUsageDescription` (local relays), and configure `AVAudioSession` yourself (`.playAndRecord` to publish, `.playback` to watch) — `MicrophoneCapture` will not. Android: declare + request `CAMERA`\u002F`RECORD_AUDIO` (`MicrophoneCapture.start()` throws on startup failure), `INTERNET`, and the MediaProjection foreground-service setup for screen capture.\n7. **Own your references and clean up.** iOS: keep strong refs to `Session`, `BroadcastSubscription`, `Publisher`, `DataTrackEmitter`, `Player`, and every event\u002Fstats subscription; hold the `Task` handles that drive `for await` loops (the `AsyncStream`s are single-consumer, too). Android: nearly everything is `AutoCloseable` and holds ref-counted native handles — `close()` each `Broadcast`, `BroadcastSubscription`, `TrackSubscription`, `MediaTrack`, `AudioDataStream`, and `Player` (leaking one keeps the broadcast alive). `broadcasts`, `objects`, `frames`, and `audio` flows allow a **single collector**.\n8. **iOS `Player` is `@MainActor`** — construct and drive it on the main actor. Capture and publication async controls move blocking setup\u002Fteardown off the main thread; await their completion.\n9. **When one session both publishes and subscribes the same prefix, it sees its own broadcast** — filter it out, remembering reported paths are prefix-relative: compare against your publish path with the subscribe prefix stripped. Two `Session`s in one process (one publishing, one subscribing) is also supported — and since sessions are one-shot, separate ones keep a transport failure or teardown on the publish side from taking playback down with it.\n10. **Latency is tunable live**: `targetBuffering` defaults to 100 ms; `player.updateTargetLatency(_:)` adjusts during playback. A second consumer of the same media track (another `Player`, an `AudioDataStream`) reuses the first subscriber's upstream buffering — it can't independently lower it.\n11. **Custom sources must share the publisher's clock domain.** A `Publisher` stamps all of its tracks against one epoch, set by the first frame of *any* track, so a source emitting its own zero-based timeline drifts against the mic or leaves video scheduled far in the future. iOS: PTS on the host clock (`CMClockGetTime(CMClockGetHostTimeClock())`) — the domain AVFoundation capture already uses. Android: `SystemClock.elapsedRealtimeNanos() \u002F 1_000` for PCM (what `MicrophoneCapture` uses) and the same clock behind the encoder surface's presentation timestamps.\n\n## Symptom → cause\n\n| Symptom | Likely cause |\n| --- | --- |\n| No broadcasts ever arrive | Relay URL missing the namespace path (`\u002Fanon` locally) — the relay answers `NotFound` (`code=13`) and nothing surfaces; or `subscribe()` ran before the session reached connected |\n| Publishing \"works\" but subscribers get no media | Captures were never started — `publisher.start()` does not start them (rule 3) |\n| No microphone audio | iOS: `AVAudioSession` isn't on a record-capable category. Android: check `RECORD_AUDIO` and the error thrown by `MicrophoneCapture.start()` |\n| Android: audio plays, video is black | No surface set, or a stale one after `surfaceDestroyed` — null it there and only re-apply a surface that `isValid` |\n| iOS: audio plays, video is black | `player.videoLayer` was never added to a layer hierarchy or never sized — nothing renders until it's in the tree and `play()` has run |\n| `track.ready` \u002F `playback.start` never fire | Subscribed after `play()`, or (iOS) the returned `PlayerEventSubscription` wasn't retained |\n| Data-track sends disappear | Sent before `publisher.start()` or after `stop()` — dropped silently on both platforms; gate sends on publisher state |\n| Nothing recovers after a network blip | Sessions are one-shot; on Android a `Closed` you didn't ask for **is** the failure signal. Build a new `Session` — but log `Error(message)` if you see it, it carries the only description of what went wrong |\n| A remote error carries a bare `code=N` | Codes are `moq_net::Error` discriminants: `13` NotFound, `24` Dropped \"producer dropped without finishing\", `25` Closed, `26` Lagged; full list in moq-net's `error.rs` (`vendor\u002Fmoq\u002Frs\u002Fmoq-net\u002Fsrc\u002Ferror.rs` in a moq-kit checkout) |\n| Publication still active during teardown | Await `Publisher.stop()` \u002F `PublishedTrack.stop()`; canceling UI observation alone does not stop media |\n| A publisher's own broadcast shows up in its subscriber list | Same session publishing and subscribing the same prefix (rule 9) — filter by prefix-relative path |\n| Custom source: A\u002FV drifts apart, or video freezes while audio keeps playing | Frame timestamps aren't in the same clock domain as the publisher's other tracks (rule 11) |\n| Android: `play()` throws `IllegalArgumentException` | It's an `UnsupportedCodecException` (a subclass) for an undecodable selected track — catch that first |\n| Android: screen capture starts, then dies | The `mediaProjection` foreground service wasn't running yet, or was started outside the consent-result callback (Android 14+ rejects that) |\n| Device or emulator can't reach a local relay | `localhost` is the device itself — use `10.0.2.2` from the Android emulator, the host's LAN IP from physical devices |\n\n## Debugging\n\nRelay, subscription, and publish failures usually surface in the native transport log before they show up as state. Raise the level before touching any other API:\n\n```swift\nKitLogger.setNativeLogLevel(\"debug\")   \u002F\u002F \"error\"\u002F\"warn\"\u002F\"info\"\u002F\"debug\"\u002F\"trace\"; only the first call takes effect\n```\n\n```kotlin\nNativeLogging.setLogLevel(\"debug\")     \u002F\u002F same levels; invalid values are ignored with a warning\n```\n\nAndroid native logs land in **logcat under the tag `MoQNative`**, but only when the native library was built with the `android-logcat` cargo feature (`moq-ffi` → `moq-native` → `tracing-android`); without it the `tracing` subscriber writes to stderr, which Android discards. A successful `setLogLevel` call with no `MoQNative` lines afterwards means the feature is off in the artifact you are using — fall back to session\u002Fpublisher state and player events there.\n\niOS also logs through `os_log` under subsystem `com.swmansion.MoQKit` (categories `session`, `transport`, `catalog`, `media`, `player`, `publish`) — filter Console.app on it. Per-frame drop\u002Fstall detail is on the player's diagnostics channel, not in these logs.\n\nThe iOS `publish` category logs track state and enable changes, camera\u002Fmicrophone start and stop, capture notifications, and runtime errors. The iOS demo logs publisher errors under subsystem `com.swmansion.MoQDemo`, category `publisher-demo`.\n\n## Testing against a relay\n\nFrom a moq-kit checkout (needs Rust + [mise](https:\u002F\u002Fmise.jdx.dev)): `mise run relay:run` starts a local moq-lite relay at `http:\u002F\u002Flocalhost:4443\u002Fanon`; `mise run media:to-fmp4` converts a video to CMAF fMP4 and `mise run stream:file --input file.mp4` loops it into the relay as a broadcast. Use the machine's LAN IP instead of `localhost` from physical devices, and `10.0.2.2` from the Android emulator (the emulator's fixed alias for the host's loopback — its own `localhost` is itself; the iOS simulator shares the host's). The relay URL must include the namespace path (`\u002Fanon` locally) — with it wrong or missing, broadcasts simply never appear (relay `NotFound`, `code=13`). The demo apps (`examples\u002Fios\u002Fdemo\u002FMoQDemo`, `examples\u002Fandroid\u002Fdemo\u002FMoQDemo`) are the canonical integration references.\n\n## References\n\n| Task | iOS (Swift) | Android (Kotlin) |\n| --- | --- | --- |\n| Playback: session, discovery, Player, rendering, events\u002Fstats wiring | [references\u002Fplayback-ios.md](references\u002Fplayback-ios.md) | [references\u002Fplayback-android.md](references\u002Fplayback-android.md) |\n| Publishing: camera, multi-camera, microphone, previews, encoder configs, permissions | [references\u002Fpublishing-ios.md](references\u002Fpublishing-ios.md) | [references\u002Fpublishing-android.md](references\u002Fpublishing-android.md) |\n| Screen sharing | [references\u002Fscreen-capture-ios.md](references\u002Fscreen-capture-ios.md) | [references\u002Fscreen-capture-android.md](references\u002Fscreen-capture-android.md) |\n| Data tracks, raw compressed media (MediaTrack), decoded PCM (AudioDataStream) | [references\u002Fdata-tracks-ios.md](references\u002Fdata-tracks-ios.md) | [references\u002Fdata-tracks-android.md](references\u002Fdata-tracks-android.md) |\n","# moq-kit\n\nNative Swift and Kotlin SDKs for Media over QUIC live streaming: connect to a relay, discover broadcasts, play catalog-described streams with sub-second latency, publish camera\u002Fmicrophone\u002Fscreen tracks, and exchange raw data tracks. Wraps the UniFFI bindings generated from `moq-ffi` (Luke Curley's `moq-dev\u002Fmoq`); targets the `moq-lite` protocol, not the IETF `moq-transport` draft.\n\n**Requirements:** iOS 16+ \u002F macOS 13+ (Xcode 16+ to build) \u002F Android minSdk 29, compileSdk 35. A moq-lite relay to connect to. The references cover iOS and Android; macOS is declared by the Swift package but untested here.\n\nmoq-kit is an **active preview**: public APIs, packaging, and codec coverage can still change between releases. Pin an exact version and check signatures against the installed artifact rather than assuming a shape.\n\n**Install — iOS (SPM):**\n\n```swift\n.package(url: \"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\", exact: \"\u003Clatest release>\")\n\u002F\u002F product: .product(name: \"MoQKit\", package: \"moq-kit\")\n```\n\n**Install — Android (Maven Central):**\n\n```kotlin\ndependencies { implementation(\"com.swmansion.moqkit:moqkit:\u003Clatest release>\") }\n```\n\nFill in `\u003Clatest release>` from https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Freleases. Both pins are exact on purpose — loosen the SPM one (`.upToNextMinor(from:)`) only if you mean to track preview releases as they land.\n\n## Platform detection\n\nReference files are split per platform — read only the ones for the project at hand:\n\n- `Package.swift`, `*.xcodeproj`, or a `Podfile` → the `*-ios.md` references (Swift).\n- `build.gradle`\u002F`build.gradle.kts` with `com.swmansion.moqkit` → the `*-android.md` references (Kotlin).\n- Cross-platform work (feature parity, bindings on top of both SDKs) → read both; the APIs are deliberately kept equivalent.\n\n## Mental model\n\n- A **`Session`** owns one QUIC connection to a relay and serves both subscribing and publishing. `connect()` once; a session is **one-shot** — after `close()` (or a connection error) create a new `Session`. Swift: `actor`, `connect() async throws`. Kotlin: optionally takes a `parentScope` (defaults to an internal IO scope; pass `viewModelScope`\u002F`lifecycleScope` so cancelling it tears the session down); `connect()` is the only suspend call.\n- **Subscribing:** `session.subscribe(prefix:)` → stream of `Broadcast` → `broadcast.catalogs()` → pick playable tracks → `Player(catalog:videoTrackName:audioTrackName:targetBuffering:)` → `play()`. Rendering is platform-specific: iOS adds `player.videoLayer` (an `AVSampleBufferDisplayLayer`) to the view hierarchy; Android calls `player.setSurface(surface)` with a `Surface` you own.\n- **Publishing:** create capture sources (`CameraCapture`, `MicrophoneCapture`, `MultiCameraCapture`, `ScreenCapture`) (start them explicitly when hardware is wanted) → `Publisher()` + `addVideoTrack`\u002F`addAudioTrack`\u002F`addDataTrack` → `session.publish(path, publisher)` → `publisher.start()`. A publisher is single-use; add all tracks before `start()`.\n- **Data tracks** (`DataTrackEmitter` → `addDataTrack`; consume with `broadcast.subscribeTrack(name:)`) bypass the media catalog — publisher and subscriber agree on the track name out of band.\n- Everything is observable: `session.state`, `publisher.state`\u002F`events`, player events\u002Fstats. Kotlin uses `StateFlow`\u002F`SharedFlow`\u002Fcold `Flow`; Swift uses `AsyncStream` for session\u002Fpublisher state and diagnostics, but callback subscriptions (`subscribeEvents`\u002F`subscribeStats`) for player events\u002Fstats.\n\n## Quick start — watch\n\n**iOS:**\n\n```swift\nlet session = Session(url: \"http:\u002F\u002Flocalhost:4443\u002Fanon\")\ntry await session.connect()\nlet subscription = try await session.subscribe(prefix: \"live\")\nfor await broadcast in subscription.broadcasts {\n    for await catalog in broadcast.catalogs() {\n        let video = catalog.playableVideoTracks.first?.name\n        let audio = catalog.playableAudioTracks.first?.name\n        guard video != nil || audio != nil else { continue }\n        let player = try await MainActor.run {\n            try Player(catalog: catalog, videoTrackName: video, audioTrackName: audio)\n        }\n        try await player.play() \u002F\u002F render via player.videoLayer — see playback-ios.md\n    }\n}\n```\n\n**Android:**\n\n```kotlin\nscope.launch {\n    val session = Session(url = \"http:\u002F\u002Flocalhost:4443\u002Fanon\", parentScope = scope)\n    session.connect()\n    session.subscribe(prefix = \"live\").broadcasts.collect { broadcast ->\n        broadcast.catalogs().collect { catalog ->\n            val video = catalog.playableVideoTracks.firstOrNull()?.name\n            val audio = catalog.playableAudioTracks.firstOrNull()?.name\n            if (video == null && audio == null) return@collect\n            val player = Player(catalog, video, audio, parentScope = scope)\n            player.setSurface(surfaceView.holder.surface)\n            player.play()\n        }\n    }\n}\n```\n\nMinimal on purpose: the nested loops handle one broadcast at a time — spawn a `Task`\u002F`launch` per broadcast in real code — and each catalog update should tear down the previous player before creating the new one (players hold native handles).\n\n## Quick start — go live\n\nSame four steps on both platforms, and the order is load-bearing (rule 3):\n\n**iOS:**\n\n```swift\ntry await camera.start()                                   \u002F\u002F 1. captures — publisher.start() won't do this\nlet publisher = try Publisher()                            \u002F\u002F 2. every track before start()\ntry publisher.addVideoTrack(name: \"camera\", source: camera, config: VideoEncoderConfig())\ntry await session.publish(path: \"live\u002Fios\", publisher: publisher)  \u002F\u002F 3. register\ntry await publisher.start()                                \u002F\u002F 4. go live\n```\n\n**Android:**\n\n```kotlin\ncamera.start(context, lifecycleOwner)\nval publisher = Publisher()\npublisher.addVideoTrack(name = \"camera\", source = camera, config = VideoEncoderConfig())\nsession.publish(path = \"live\u002Fandroid\", publisher = publisher)\npublisher.start()\n```\n\nCaptures, previews, encoder configs, and permissions are in the publishing references.\n\n## Shared concepts (both platforms)\n\n- **Catalogs:** `broadcast.catalogs()` streams catalog updates — each element **replaces** the previous; the stream ending means the broadcast went away (mark it offline). It often ends by **throwing** rather than completing — a producer that disappears surfaces as `MoqException$Mux: json: remote error: code=…` out of `collect`, so handle the exception and the clean completion identically (both demo apps and react-native-moq do). Always select from `playableVideoTracks` \u002F `playableAudioTracks` (device-filtered on Android and for iOS audio; iOS video playability is codec-family recognition only), not the raw lists; per-track `isPlayable`\u002F`unsupportedReason` explain exclusions (they live on the concrete `VideoTrackInfo`\u002F`AudioTrackInfo`, not the base `TrackInfo`). Video codec strings: `\"avc1\"`, `\"hev1\"`, `\"av01\"`; audio: `\"mp4a.40.2\"`\u002F`\"aac\"` (Android-published tracks advertise `\"aac\"`), `\"opus\"`. AV1 is **playback-only** — neither platform publishes it, and Apple-side decode is iPhone 15 Pro-class hardware, not a general capability.\n- **Broadcast paths are prefix-relative:** subscribing with prefix `\"live\"` reports broadcast `live\u002Fgame` as `path: \"game\"` — subscribe with `\"\"` to see full paths.\n- **Three observation channels on `Player`** — don't mix them up: **events** (lifecycle milestones — `player.init`, `playback.start`, `track.ready`, `track.switch`, `track.stall.start`, `rebuffer.end`, `decode.error` — wire values; Kotlin matches the `PlayerEventName` enum; not replayed), **stats** (`PlaybackStats` at ~1 s cadence — latency, fps, bitrates, `StallStats`, `timeToFirst`, buffer depths, drops, switches), and **diagnostics** (typed per-frame `PipelineEvent` — `frameDropped` with `DropStage`\u002F`DropReason`, `stallStarted`, `latencySample`, … — bounded at 256 drop-oldest — Android hands every caller the same shared flow — and never backpressures playback; for logging\u002Ftelemetry only).\n- **Publisher states:** `Idle \u002F Publishing \u002F Stopped \u002F Error(message)`. Session teardown stops its publishers. Audio\u002Fvideo return `PublishedMediaTrack` with requested `isEnabled` and async\u002Fsuspend `setEnabled(bool)`; states are `Idle \u002F Disabled \u002F Starting \u002F Active \u002F Failed(message) \u002F Stopped`. Raw data returns `PublishedTrack` with state and terminal `stop()` only. Publisher, track, and capture cleanup awaits teardown. Empty publishers stay open until explicit stop or session end.\n- **`TrackDelivery`** for raw track subscription: `monotonic` (default) skips late groups — right for newest-value-wins live state; `arrival` delivers every group in order — right for chat and anything lossless.\n\n## Critical rules\n\n1. **Sessions, publishers, and players are single-use.** A `Session` connects once (`alreadyConnected`\u002F`IllegalStateException` on reuse); after `close()` make a new one. A stopped `Publisher` cannot be restarted. Never restart a torn-down `Player` on either platform — create a fresh one.\n2. **Media track names are local labels, catalog names are muxer-generated.** The `name:` passed to `addVideoTrack`\u002F`addAudioTrack` (defaults `\"video\"`\u002F`\"audio\"`) only labels `PublishedTrack` and publisher events; subscribers must discover real track names from `Catalog.videoTracks`\u002F`audioTracks`. **Exception: data tracks** — `addDataTrack(name:)` (default `\"data\"`) IS the name subscribers pass to `subscribeTrack(name:)`.\n3. **`publisher.start()` does not start capture.** Add tracks → `session.publish(path, publisher)` → `publisher.start()`. Camera\u002Fmicrophone may be started before or after registration. Capture stop suspends enabled publication and releases hardware; track `setEnabled(false)` releases only encoding\u002Fpublication. Capture start resumes enabled tracks.\n4. **Gate codecs before use.** `VideoEncoderConfig.supportedCodecs()` \u002F `AudioEncoderConfig.supportedCodecs()` \u002F `.isSupported` on the publish side; `catalog.playableVideoTracks` \u002F `playableAudioTracks` \u002F per-track `isPlayable` on the playback side. Unsupported configs throw (`SessionError.unsupportedCodec` \u002F `UnsupportedCodecException`). H.264 + AAC\u002FOpus are the best-tested paths; default to H.264. Opus wants 48 kHz — on Android keep `MicrophoneCapture(sampleRate = …)` equal to the encoder config; iOS has no mic rate knob and resamples internally.\n5. **Player startup events are not replayed.** Subscribe before `play()`: iOS `player.subscribeEvents { ... }` (and **retain** the returned subscription — it cancels on deinit); Android collect `player.events` with `CoroutineStart.UNDISPATCHED`. On both platforms `player.init` fires during construction and can never be observed — the rule protects `track.ready`\u002F`playback.start`.\n6. **Permissions and audio sessions are the app's job.** iOS: `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, `NSLocalNetworkUsageDescription` (local relays), and configure `AVAudioSession` yourself (`.playAndRecord` to publish, `.playback` to watch) — `MicrophoneCapture` will not. Android: declare + request `CAMERA`\u002F`RECORD_AUDIO` (`MicrophoneCapture.start()` throws on startup failure), `INTERNET`, and the MediaProjection foreground-service setup for screen capture.\n7. **Own your references and clean up.** iOS: keep strong refs to `Session`, `BroadcastSubscription`, `Publisher`, `DataTrackEmitter`, `Player`, and every event\u002Fstats subscription; hold the `Task` handles that drive `for await` loops (the `AsyncStream`s are single-consumer, too). Android: nearly everything is `AutoCloseable` and holds ref-counted native handles — `close()` each `Broadcast`, `BroadcastSubscription`, `TrackSubscription`, `MediaTrack`, `AudioDataStream`, and `Player` (leaking one keeps the broadcast alive). `broadcasts`, `objects`, `frames`, and `audio` flows allow a **single collector**.\n8. **iOS `Player` is `@MainActor`** — construct and drive it on the main actor. Capture and publication async controls move blocking setup\u002Fteardown off the main thread; await their completion.\n9. **When one session both publishes and subscribes the same prefix, it sees its own broadcast** — filter it out, remembering reported paths are prefix-relative: compare against your publish path with the subscribe prefix stripped. Two `Session`s in one process (one publishing, one subscribing) is also supported — and since sessions are one-shot, separate ones keep a transport failure or teardown on the publish side from taking playback down with it.\n10. **Latency is tunable live**: `targetBuffering` defaults to 100 ms; `player.updateTargetLatency(_:)` adjusts during playback. A second consumer of the same media track (another `Player`, an `AudioDataStream`) reuses the first subscriber's upstream buffering — it can't independently lower it.\n11. **Custom sources must share the publisher's clock domain.** A `Publisher` stamps all of its tracks against one epoch, set by the first frame of *any* track, so a source emitting its own zero-based timeline drifts against the mic or leaves video scheduled far in the future. iOS: PTS on the host clock (`CMClockGetTime(CMClockGetHostTimeClock())`) — the domain AVFoundation capture already uses. Android: `SystemClock.elapsedRealtimeNanos() \u002F 1_000` for PCM (what `MicrophoneCapture` uses) and the same clock behind the encoder surface's presentation timestamps.\n\n## Symptom → cause\n\n| Symptom | Likely cause |\n| --- | --- |\n| No broadcasts ever arrive | Relay URL missing the namespace path (`\u002Fanon` locally) — the relay answers `NotFound` (`code=13`) and nothing surfaces; or `subscribe()` ran before the session reached connected |\n| Publishing \"works\" but subscribers get no media | Captures were never started — `publisher.start()` does not start them (rule 3) |\n| No microphone audio | iOS: `AVAudioSession` isn't on a record-capable category. Android: check `RECORD_AUDIO` and the error thrown by `MicrophoneCapture.start()` |\n| Android: audio plays, video is black | No surface set, or a stale one after `surfaceDestroyed` — null it there and only re-apply a surface that `isValid` |\n| iOS: audio plays, video is black | `player.videoLayer` was never added to a layer hierarchy or never sized — nothing renders until it's in the tree and `play()` has run |\n| `track.ready` \u002F `playback.start` never fire | Subscribed after `play()`, or (iOS) the returned `PlayerEventSubscription` wasn't retained |\n| Data-track sends disappear | Sent before `publisher.start()` or after `stop()` — dropped silently on both platforms; gate sends on publisher state |\n| Nothing recovers after a network blip | Sessions are one-shot; on Android a `Closed` you didn't ask for **is** the failure signal. Build a new `Session` — but log `Error(message)` if you see it, it carries the only description of what went wrong |\n| A remote error carries a bare `code=N` | Codes are `moq_net::Error` discriminants: `13` NotFound, `24` Dropped \"producer dropped without finishing\", `25` Closed, `26` Lagged; full list in moq-net's `error.rs` (`vendor\u002Fmoq\u002Frs\u002Fmoq-net\u002Fsrc\u002Ferror.rs` in a moq-kit checkout) |\n| Publication still active during teardown | Await `Publisher.stop()` \u002F `PublishedTrack.stop()`; canceling UI observation alone does not stop media |\n| A publisher's own broadcast shows up in its subscriber list | Same session publishing and subscribing the same prefix (rule 9) — filter by prefix-relative path |\n| Custom source: A\u002FV drifts apart, or video freezes while audio keeps playing | Frame timestamps aren't in the same clock domain as the publisher's other tracks (rule 11) |\n| Android: `play()` throws `IllegalArgumentException` | It's an `UnsupportedCodecException` (a subclass) for an undecodable selected track — catch that first |\n| Android: screen capture starts, then dies | The `mediaProjection` foreground service wasn't running yet, or was started outside the consent-result callback (Android 14+ rejects that) |\n| Device or emulator can't reach a local relay | `localhost` is the device itself — use `10.0.2.2` from the Android emulator, the host's LAN IP from physical devices |\n\n## Debugging\n\nRelay, subscription, and publish failures usually surface in the native transport log before they show up as state. Raise the level before touching any other API:\n\n```swift\nKitLogger.setNativeLogLevel(\"debug\")   \u002F\u002F \"error\"\u002F\"warn\"\u002F\"info\"\u002F\"debug\"\u002F\"trace\"; only the first call takes effect\n```\n\n```kotlin\nNativeLogging.setLogLevel(\"debug\")     \u002F\u002F same levels; invalid values are ignored with a warning\n```\n\nAndroid native logs land in **logcat under the tag `MoQNative`**, but only when the native library was built with the `android-logcat` cargo feature (`moq-ffi` → `moq-native` → `tracing-android`); without it the `tracing` subscriber writes to stderr, which Android discards. A successful `setLogLevel` call with no `MoQNative` lines afterwards means the feature is off in the artifact you are using — fall back to session\u002Fpublisher state and player events there.\n\niOS also logs through `os_log` under subsystem `com.swmansion.MoQKit` (categories `session`, `transport`, `catalog`, `media`, `player`, `publish`) — filter Console.app on it. Per-frame drop\u002Fstall detail is on the player's diagnostics channel, not in these logs.\n\nThe iOS `publish` category logs track state and enable changes, camera\u002Fmicrophone start and stop, capture notifications, and runtime errors. The iOS demo logs publisher errors under subsystem `com.swmansion.MoQDemo`, category `publisher-demo`.\n\n## Testing against a relay\n\nFrom a moq-kit checkout (needs Rust + [mise](https:\u002F\u002Fmise.jdx.dev)): `mise run relay:run` starts a local moq-lite relay at `http:\u002F\u002Flocalhost:4443\u002Fanon`; `mise run media:to-fmp4` converts a video to CMAF fMP4 and `mise run stream:file --input file.mp4` loops it into the relay as a broadcast. Use the machine's LAN IP instead of `localhost` from physical devices, and `10.0.2.2` from the Android emulator (the emulator's fixed alias for the host's loopback — its own `localhost` is itself; the iOS simulator shares the host's). The relay URL must include the namespace path (`\u002Fanon` locally) — with it wrong or missing, broadcasts simply never appear (relay `NotFound`, `code=13`). The demo apps (`examples\u002Fios\u002Fdemo\u002FMoQDemo`, `examples\u002Fandroid\u002Fdemo\u002FMoQDemo`) are the canonical integration references.\n\n## References\n\n| Task | iOS (Swift) | Android (Kotlin) |\n| --- | --- | --- |\n| Playback: session, discovery, Player, rendering, events\u002Fstats wiring | [references\u002Fplayback-ios.md](references\u002Fplayback-ios.md) | [references\u002Fplayback-android.md](references\u002Fplayback-android.md) |\n| Publishing: camera, multi-camera, microphone, previews, encoder configs, permissions | [references\u002Fpublishing-ios.md](references\u002Fpublishing-ios.md) | [references\u002Fpublishing-android.md](references\u002Fpublishing-android.md) |\n| Screen sharing | [references\u002Fscreen-capture-ios.md](references\u002Fscreen-capture-ios.md) | [references\u002Fscreen-capture-android.md](references\u002Fscreen-capture-android.md) |\n| Data tracks, raw compressed media (MediaTrack), decoded PCM (AudioDataStream) | [references\u002Fdata-tracks-ios.md](references\u002Fdata-tracks-ios.md) | [references\u002Fdata-tracks-android.md](references\u002Fdata-tracks-android.md) |\n",{"name":5,"license":15,"description":7},"main","296748faadf0e5856e42aa6078c6f48fc6788b9e","skills\u002Fmoq-kit",6,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs","https:\u002F\u002Fswmansion.com","Our experimental projects are hosted here. For our officially maintained libraries visit https:\u002F\u002Fgithub.com\u002Fsoftware-mansion.","2026-09-28 19:16:58",{"source":8,"skillName":5,"commands":54,"prompt":64,"zipAvailable":65},[55,58,61],{"tool":56,"command":57},"npx","npx skills add software-mansion-labs\u002Fmoq-kit --skill moq-kit",{"tool":59,"command":60},"bunx","bunx skills add software-mansion-labs\u002Fmoq-kit --skill moq-kit",{"tool":62,"command":63},"pnpm","pnpm dlx skills add software-mansion-labs\u002Fmoq-kit --skill moq-kit","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002FSKILL.md",true,[67,72,78,82,86,90,94,98,102,106,110,114],{"path":68,"resourceType":68,"resourceTypeDesc":69,"sizeBytes":70,"sourceHtmlUrl":71},"LICENSE","许可证",1065,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-navigation\u002FLICENSE",{"path":73,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":76,"sourceHtmlUrl":77},"references\u002Fdata-tracks-android.md","REFERENCE","参考文档",2709,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fdata-tracks-android.md",{"path":79,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":80,"sourceHtmlUrl":81},"references\u002Fdata-tracks-ios.md",2448,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fdata-tracks-ios.md",{"path":83,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":84,"sourceHtmlUrl":85},"references\u002Fplayback-android.md",5562,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fplayback-android.md",{"path":87,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":88,"sourceHtmlUrl":89},"references\u002Fplayback-ios.md",4978,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fplayback-ios.md",{"path":91,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":92,"sourceHtmlUrl":93},"references\u002Fpublishing-android.md",7791,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fpublishing-android.md",{"path":95,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":96,"sourceHtmlUrl":97},"references\u002Fpublishing-ios.md",7794,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fpublishing-ios.md",{"path":99,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":100,"sourceHtmlUrl":101},"references\u002Fscreen-capture-android.md",1939,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fscreen-capture-android.md",{"path":103,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":104,"sourceHtmlUrl":105},"references\u002Fscreen-capture-ios.md",2656,"https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002Fskills\u002Fmoq-kit\u002Freferences\u002Fscreen-capture-ios.md",{"path":107,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":108,"sourceHtmlUrl":109},"repos\u002Fmetamask-mobile.md",2745,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-navigation\u002Frepos\u002Fmetamask-mobile.md",{"path":111,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":112,"sourceHtmlUrl":113},"SOURCE.md",362,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-navigation\u002FSOURCE.md",{"path":115,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":116,"sourceHtmlUrl":117},"TRUST.auto.yaml",2217,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-navigation\u002FTRUST.auto.yaml",[119,131,139,149,158,168],{"skillNo":120,"name":121,"nameZh":6,"description":122,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":123,"categories":124,"keywords":125,"publishedAt":39},"01M6CSNTAMZMSZ272RPREGN43S","run-ios","Build and run the iOS MoQ Demo on a connected device","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Frun-ios\u002FSKILL.md",[],[126,30,127,128,129,130],"run ios","run","device","build","demo",{"skillNo":132,"name":133,"nameZh":6,"description":134,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":135,"categories":136,"keywords":137,"publishedAt":39},"01M6CSNRKG3B3KG1MWFDBSZJ5K","run-android","Build and run the Android subscriber demo on a connected device","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Frun-android\u002FSKILL.md",[],[138,127,38,129,128,130],"run android",{"skillNo":140,"name":141,"nameZh":6,"description":142,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":143,"categories":144,"keywords":145,"publishedAt":39},"01M6CSNQEF96M1W4FZ34D57MYG","commit","Analyzes the staged\u002Funstaged git files and commits them","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fcommit\u002FSKILL.md",[],[141,146,147,148],"files","commits","git",{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":39},"01M6CSNNQNW488TZ53J0Q0SXV3","build-rust","Build Rust moq-ffi for iOS\u002FAndroid targets","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fbuild-rust\u002FSKILL.md",[],[156,129,157,38,31,30],"build rust","rust",{"skillNo":159,"name":160,"nameZh":6,"description":161,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":162,"categories":163,"keywords":164,"publishedAt":39},"01M6CSNMBMHHNVW4BEKPFX6VYN","build-ios","Build iOS Swift package","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fbuild-ios\u002FSKILL.md",[],[165,30,129,166,167],"build ios","swift","package",{"skillNo":169,"name":170,"nameZh":6,"description":171,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":172,"categories":173,"keywords":174,"publishedAt":39},"01M6CSNJ7WSJPD84BBZJE3H1AN","build-android","Build Android Kotlin bindings, moq library and final library AAR file","https:\u002F\u002Fgithub.com\u002Fsoftware-mansion-labs\u002Fmoq-kit\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fbuild-android\u002FSKILL.md",[],[38,129,175,176,177,27],"build android","library","file"]