Loading...
Loading...
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/microphone/screen 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/Player/Publisher/Catalog from moq-kit.
npx skill4agent add software-mansion-labs/skills moq-kitmoq-ffimoq-dev/moqmoq-litemoq-transport.package(url: "https://github.com/software-mansion-labs/moq-kit", exact: "<latest release>")
// product: .product(name: "MoQKit", package: "moq-kit")dependencies { implementation("com.swmansion.moqkit:moqkit:<latest release>") }<latest release>.upToNextMinor(from:)Package.swift*.xcodeprojPodfile*-ios.mdbuild.gradlebuild.gradle.ktscom.swmansion.moqkit*-android.mdSessionconnect()close()Sessionactorconnect() async throwsparentScopeviewModelScopelifecycleScopeconnect()session.subscribe(prefix:)Broadcastbroadcast.catalogs()Player(catalog:videoTrackName:audioTrackName:targetBuffering:)play()player.videoLayerAVSampleBufferDisplayLayerplayer.setSurface(surface)SurfaceCameraCaptureMicrophoneCaptureMultiCameraCaptureScreenCapturePublisher()addVideoTrackaddAudioTrackaddDataTracksession.publish(path, publisher)publisher.start()start()DataTrackEmitteraddDataTrackbroadcast.subscribeTrack(name:)session.statepublisher.stateeventsStateFlowSharedFlowFlowAsyncStreamsubscribeEventssubscribeStatslet session = Session(url: "http://localhost:4443/anon")
try await session.connect()
let subscription = try await session.subscribe(prefix: "live")
for await broadcast in subscription.broadcasts {
for await catalog in broadcast.catalogs() {
let video = catalog.playableVideoTracks.first?.name
let audio = catalog.playableAudioTracks.first?.name
guard video != nil || audio != nil else { continue }
let player = try await MainActor.run {
try Player(catalog: catalog, videoTrackName: video, audioTrackName: audio)
}
try await player.play() // render via player.videoLayer — see playback-ios.md
}
}scope.launch {
val session = Session(url = "http://localhost:4443/anon", parentScope = scope)
session.connect()
session.subscribe(prefix = "live").broadcasts.collect { broadcast ->
broadcast.catalogs().collect { catalog ->
val video = catalog.playableVideoTracks.firstOrNull()?.name
val audio = catalog.playableAudioTracks.firstOrNull()?.name
if (video == null && audio == null) return@collect
val player = Player(catalog, video, audio, parentScope = scope)
player.setSurface(surfaceView.holder.surface)
player.play()
}
}
}Tasklaunchtry await camera.start() // 1. captures — publisher.start() won't do this
let publisher = try Publisher() // 2. every track before start()
publisher.addVideoTrack(name: "camera", source: camera, config: VideoEncoderConfig())
try await session.publish(path: "live/ios", publisher: publisher) // 3. register
try await publisher.start() // 4. go livecamera.start(context, lifecycleOwner)
val publisher = Publisher()
publisher.addVideoTrack(name = "camera", source = camera, config = VideoEncoderConfig())
session.publish(path = "live/android", publisher = publisher)
publisher.start()broadcast.catalogs()MoqException$Mux: json: remote error: code=…collectplayableVideoTracksplayableAudioTracksisPlayableunsupportedReasonVideoTrackInfoAudioTrackInfoTrackInfo"avc1""hev1""av01""mp4a.40.2""aac""aac""opus""live"live/gamepath: "game"""Playerplayer.initplayback.starttrack.readytrack.switchtrack.stall.startrebuffer.enddecode.errorPlayerEventNamePlaybackStatsStallStatstimeToFirstPipelineEventframeDroppedDropStageDropReasonstallStartedlatencySampleIdle / Publishing / Stopped / Error(message)ErrorPublishingTrackStartedTrackStoppedTrackError(name, message).error(name, message)PublishedTrack.stateIdle / Starting / Active / StoppedPublishedTrack.stop()session.unpublish(path)publisher.stop()TrackDeliverymonotonicarrivalSessionalreadyConnectedIllegalStateExceptionclose()PublisherPlayername:addVideoTrackaddAudioTrack"video""audio"PublishedTrackCatalog.videoTracksaudioTracksaddDataTrack(name:)"data"subscribeTrack(name:)publisher.start()CameraCaptureMicrophoneCapturesession.publish(path, publisher)publisher.start()VideoEncoderConfig.supportedCodecs()AudioEncoderConfig.supportedCodecs().isSupportedcatalog.playableVideoTracksplayableAudioTracksisPlayableSessionError.unsupportedCodecUnsupportedCodecExceptionMicrophoneCapture(sampleRate = …)play()player.subscribeEvents { ... }player.eventsCoroutineStart.UNDISPATCHEDplayer.inittrack.readyplayback.startNSCameraUsageDescriptionNSMicrophoneUsageDescriptionNSLocalNetworkUsageDescriptionAVAudioSession.playAndRecord.playbackMicrophoneCaptureCAMERARECORD_AUDIOMicrophoneCapture.start()INTERNETSessionBroadcastSubscriptionPublisherDataTrackEmitterPlayerTaskfor awaitAsyncStreamAutoCloseableclose()BroadcastBroadcastSubscriptionTrackSubscriptionMediaTrackAudioDataStreamPlayerbroadcastsobjectsframesaudioPlayer@MainActorPublisher.stop()Task.detachedPublisherSendablenonisolated(unsafe)SessiontargetBufferingplayer.updateTargetLatency(_:)PlayerAudioDataStreamPublisherCMClockGetTime(CMClockGetHostTimeClock())SystemClock.elapsedRealtimeNanos() / 1_000MicrophoneCapture| Symptom | Likely cause |
|---|---|
| No broadcasts ever arrive | Relay URL missing the namespace path ( |
| Publishing "works" but subscribers get no media | Captures were never started — |
| No microphone audio | iOS: |
| Android: audio plays, video is black | No surface set, or a stale one after |
| iOS: audio plays, video is black | |
| Subscribed after |
| Data-track sends disappear | Sent before |
| Nothing recovers after a network blip | Sessions are one-shot; on Android a |
A remote error carries a bare | Codes are |
| iOS: UI hitches when going off-air | |
| 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 |
| Custom source: A/V 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) |
Android: | It's an |
| Android: screen capture starts, then dies | The |
| Device or emulator can't reach a local relay | |
KitLogger.setNativeLogLevel("debug") // "error"/"warn"/"info"/"debug"/"trace"; only the first call takes effectNativeLogging.setLogLevel("debug") // same levels; invalid values are ignored with a warningMoQNativeandroid-logcatmoq-ffimoq-nativetracing-androidtracingsetLogLevelMoQNativeos_logcom.swmansion.MoQKitsessiontransportcatalogmediaplayerpublishmise run relay:runhttp://localhost:4443/anonmise run media:to-fmp4mise run stream:file --input file.mp4localhost10.0.2.2localhost/anonNotFoundcode=13examples/ios/demo/MoQDemoexamples/android/demo/MoQDemo| Task | iOS (Swift) | Android (Kotlin) |
|---|---|---|
| Playback: session, discovery, Player, rendering, events/stats wiring | references/playback-ios.md | references/playback-android.md |
| Publishing: camera, multi-camera, microphone, previews, encoder configs, permissions | references/publishing-ios.md | references/publishing-android.md |
| Screen sharing | references/screen-capture-ios.md | references/screen-capture-android.md |
| Data tracks, raw compressed media (MediaTrack), decoded PCM (AudioDataStream) | references/data-tracks-ios.md | references/data-tracks-android.md |