mcpField
M
moq-kit

moq-kit

github.com/software-mansion-labs開発296311 回インストール2 週間前に更新
リポジトリ

moq-kit について

moq-kitは、Media over QUICのライブ配信アプリ向けにSwiftとKotlinのネイティブSDKを提供します。moq-liteを基盤とし、iOSやAndroidでの再生、配信、データトラックに対応します。

主な機能

  1. 01SwiftとKotlinのネイティブSDK
  2. 02ブロードキャストの購読とトラック再生
  3. 03カメラ、マイク、画面のメディア配信
  4. 04生データトラックの交換
  5. 05再生と配信状態の監視

活用シーン

  1. 01ライブ配信を視聴するiOSアプリの開発
  2. 02Androidからカメラやマイクのストリームを配信
  3. 03データトラックでライブ状態やチャットを送信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/software-mansion-labs/skills --skill moq-kit

インストール後、Claude Code で/moq-kitを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
data-tracks-android.md
data-tracks-ios.md
playback-android.md
playback-ios.md
publishing-android.md
publishing-ios.md
screen-capture-android.md
screen-capture-ios.md

詳細

moq-kit

Media over QUIC ライブストリーミング向けのネイティブ Swift および Kotlin SDK です。リレーに接続し、ブロードキャストを検出し、カタログで記述されたストリームを 1 秒未満の遅延で再生し、カメラ/マイク/画面のトラックを公開し、生データトラックをやり取りできます。moq-ffi(Luke Curley の moq-dev/moq)から生成された UniFFI バインディングをラップしています。対象は moq-lite プロトコルであり、IETF の moq-transport ドラフトではありません。

要件: iOS 16+ / macOS 13+(ビルドには Xcode 16+)/ Android minSdk 29、compileSdk 35。接続先となる moq-lite リレー。リファレンスは iOS と Android を対象としています。macOS は Swift パッケージで宣言されていますが、ここではテストされていません。

moq-kit はプレビュー段階で積極的に開発中です。公開 API、パッケージング、コーデックの対応状況はリリース間で変更される可能性があります。バージョンを正確に固定し、API の形状を想定するのではなく、インストール済みの成果物でシグネチャを確認してください。

インストール — iOS(SPM):

.package(url: "https://github.com/software-mansion-labs/moq-kit", exact: "<latest release>")
// product: .product(name: "MoQKit", package: "moq-kit")

インストール — Android(Maven Central):

dependencies { implementation("com.swmansion.moqkit:moqkit:<latest release>") }

https://github.com/software-mansion-labs/moq-kit/releases から <latest release> を入力してください。どちらも意図的にバージョンを正確に固定しています。プレビューリリースが公開されるたびに追従する場合に限り、SPM の指定を緩めてください(.upToNextMinor(from:))。

プラットフォームの判定

リファレンスファイルはプラットフォームごとに分かれています。対象プロジェクトに該当するものだけを参照してください。

  • Package.swift、*.xcodeproj、または Podfile がある場合 → *-ios.md のリファレンス(Swift)。
  • com.swmansion.moqkit を含む build.gradle/build.gradle.kts がある場合 → *-android.md のリファレンス(Kotlin)。
  • クロスプラットフォームの作業(機能の同等性、両方の SDK 上に構築するバインディング)→ 両方を参照してください。API は意図的に同等に保たれています。

基本的な考え方

  • Session はリレーへの QUIC 接続を 1 つ所有し、購読と公開の両方を担います。connect() は 1 回だけ呼び出します。セッションは一度限りです。close() の後、または接続エラーの後は、新しい Session を作成してください。Swift では actor で、connect() async throws を使います。Kotlin では必要に応じて parentScope を指定できます(デフォルトは内部の IO スコープです。キャンセル時にセッションを終了させるには、viewModelScope/lifecycleScope を渡してください)。サスペンド関数は connect() のみです。
  • 購読: session.subscribe(prefix:) → Broadcast のストリーム → broadcast.catalogs() → 再生可能なトラックを選択 → Player(catalog:videoTrackName:audioTrackName:targetBuffering:) → play()。レンダリング方法はプラットフォームごとに異なります。iOS では player.videoLayer(AVSampleBufferDisplayLayer)をビュー階層に追加し、Android では所有する Surface を player.setSurface(surface) に渡します。
  • 公開: キャプチャソース(CameraCapture、MicrophoneCapture、MultiCameraCapture、ScreenCapture)を作成し、自分で開始します → Publisher() と addVideoTrack/addAudioTrack/addDataTrack → session.publish(path, publisher) → publisher.start()。Publisher は 1 回しか使用できません。start() の前にすべてのトラックを追加してください。
  • データトラック(DataTrackEmitter → addDataTrack、受信には broadcast.subscribeTrack(name:) を使用)はメディアカタログを介しません。Publisher と Subscriber は、帯域外でトラック名を合意しておきます。
  • すべての状態を監視できます。session.state、publisher.state/events、プレイヤーのイベント/統計情報などです。Kotlin では StateFlow/SharedFlow/cold Flow を使用します。Swift ではセッション/Publisher の状態と診断情報に AsyncStream を使用し、プレイヤーのイベント/統計情報にはコールバック登録(subscribeEvents/subscribeStats)を使用します。

クイックスタート — 視聴

iOS:

let 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
    }
}

Android:

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()
        }
    }
}

意図的に最小限の例にしています。ネストしたループは一度に 1 つのブロードキャストを処理します。実際のコードではブロードキャストごとに Task/launch を起動してください。また、カタログが更新されたら、新しいプレイヤーを作成する前に以前のプレイヤーを必ず終了してください(プレイヤーはネイティブハンドルを保持します)。

クイックスタート — ライブ配信

両プラットフォームで手順は同じ 4 つですが、順序が重要です(ルール 3)。

iOS:

try 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 live

Android:

camera.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() はカタログの更新をストリームで配信します。各要素は直前の要素を置き換えます。ストリームが終了したら、ブロードキャストが消失したことを意味するため、オフラインとして扱います。正常に完了するのではなく、例外をスローして終了することがよくあります。消失したプロデューサーは collect の外側に MoqException$Mux: json: remote error: code=… として現れるため、例外と正常な完了は同じように処理してください(両方のデモアプリと react-native-moq がこの処理を行います)。生のリストではなく、必ず playableVideoTracks / playableAudioTracks から選択してください(AndroidとiOSのオーディオはデバイスでフィルタリングされます。iOSのビデオはコーデックファミリーを認識するだけで再生可能と判定されます)。除外理由はトラックごとの isPlayable / unsupportedReason で確認できます。これらは基底の TrackInfo ではなく、具体的な VideoTrackInfo / AudioTrackInfo にあります。ビデオのコーデック文字列は "avc1"、"hev1"、"av01"、オーディオは "mp4a.40.2" / "aac"(Androidが公開するトラックは "aac" を通知します)、"opus" です。AV1は再生専用です。どちらのプラットフォームもAV1を公開しません。また、Apple側でのデコードにはiPhone 15 Proクラスのハードウェアが必要で、一般的な機能ではありません。
  • ブロードキャストのパスはプレフィックスを基準にします: プレフィックス "live" でサブスクライブすると、ブロードキャスト live/game は path: "game" として通知されます。完全なパスを確認するには "" でサブスクライブしてください。
  • Player の3つの監視チャネル — 混同しないでください。イベント(ライフサイクル上の節目 — player.init、playback.start、track.ready、track.switch、track.stall.start、rebuffer.end、decode.error — ワイヤー値。Kotlinでは PlayerEventName enum に対応します。再送されません)、統計情報(約1秒間隔の PlaybackStats — レイテンシー、fps、ビットレート、StallStats、timeToFirst、バッファー深度、ドロップ、切り替え)、診断情報(型付きのフレーム単位の PipelineEvent — DropStage / DropReason を含む frameDropped、stallStarted、latencySample など — 最大256件で、古いものから破棄されます — Androidではすべての呼び出し元に同じ共有フローが渡されます — 再生をバックプレッシャーで妨げることはありません。ログ記録やテレメトリー専用です)。
  • パブリッシャーの状態: Idle / Publishing / Stopped / Error(message) — ただし、Androidは実際には Error にならず、セッションの終了にも反応しません。Androidのパブリッシャーはセッションが消失した後も Publishing のままになるため、ブロードキャストが停止したことを示す唯一のシグナルはセッション自体の状態です。失敗を示すUIはイベントに連動させてください:TrackStarted / TrackStopped / トラックごとのエラー(Kotlinでは TrackError(name, message)、Swiftでは .error(name, message))。トラックごとの PublishedTrack.state(Idle / Starting / Active / Stopped)と PublishedTrack.stop() を使うと、個別のトラックを終了できます。session.unpublish(path) は publisher.stop() を呼び出します。
  • 生のトラックサブスクリプションで使う TrackDelivery:monotonic(デフォルト)は遅れて届いたグループをスキップします。最新値を優先するライブ状態に適しています。arrival はすべてのグループを順番に配信します。チャットなど、データの欠落が許されない用途に適しています。

重要なルール

  1. セッション、パブリッシャー、プレーヤーは使い捨てです。 Session は一度だけ接続できます(再利用すると alreadyConnected/IllegalStateException が発生します)。close() の後は新しいものを作成してください。停止した Publisher は再起動できません。どちらのプラットフォームでも、破棄済みの Player を再起動しないでください。新しいものを作成してください。
  2. メディアトラック名はローカルのラベルであり、カタログ名はマルチプレクサーによって生成されます。 addVideoTrack/addAudioTrack に渡す name:(デフォルトは "video"/"audio")は、PublishedTrack とパブリッシャーのイベントにラベルを付けるだけです。サブスクライバーは Catalog.videoTracks/audioTracks から実際のトラック名を取得する必要があります。例外:データトラック — addDataTrack(name:)(デフォルトは "data")の名前は、サブスクライバーが subscribeTrack(name:) に渡す名前になります。
  3. publisher.start() はキャプチャを開始しません。 CameraCapture/MicrophoneCapture などは自分で、次の順序で開始してください:キャプチャを開始 → トラックを追加 → session.publish(path, publisher) → publisher.start()。
  4. 使用前にコーデックを確認してください。 パブリッシュ側では VideoEncoderConfig.supportedCodecs()/AudioEncoderConfig.supportedCodecs()/.isSupported、再生側では catalog.playableVideoTracks/playableAudioTracks/トラックごとの isPlayable を使用してください。サポートされていない設定では例外(SessionError.unsupportedCodec/UnsupportedCodecException)が発生します。最もよくテストされているのは H.264 と AAC/Opus の組み合わせです。デフォルトでは H.264 を使用してください。Opus では 48 kHz が必要です。Android では MicrophoneCapture(sampleRate = …) をエンコーダー設定と一致させてください。iOS にはマイクのサンプルレートを指定する設定がなく、内部でリサンプリングされます。
  5. プレーヤーの起動イベントは再送されません。 play() の前に購読してください。iOS では player.subscribeEvents { ... } を使用します(返されたサブスクリプションは必ず保持してください。破棄時にキャンセルされます)。Android では CoroutineStart.UNDISPATCHED を指定して player.events を収集します。どちらのプラットフォームでも、player.init は構築中に発生するため、観測できません。このルールに従えば、track.ready/playback.start を確実に受け取れます。
  6. 権限とオーディオセッションはアプリ側で管理します。 iOS:NSCameraUsageDescription、NSMicrophoneUsageDescription、NSLocalNetworkUsageDescription(ローカルリレー用)を設定し、AVAudioSession(配信には .playAndRecord、視聴には .playback)も自分で設定してください。MicrophoneCapture は設定しません。Android:CAMERA/RECORD_AUDIO を宣言してリクエストしてください(権限がない場合、MicrophoneCapture.start() は何もせずに終了します)。また、INTERNET と画面キャプチャ用の MediaProjection フォアグラウンドサービス設定も必要です。
  7. 参照を保持し、後片付けをしてください。 iOS:Session、BroadcastSubscription、Publisher、DataTrackEmitter、Player、およびすべてのイベント/統計サブスクリプションへの強参照を保持してください。for await ループを実行する Task ハンドルも保持してください(AsyncStream も単一コンシューマーです)。Android:ほぼすべてが AutoCloseable で、参照カウントされたネイティブハンドルを保持します。各 Broadcast、BroadcastSubscription、TrackSubscription、MediaTrack、AudioDataStream、Player を close() してください(1つでもリークするとブロードキャストが存続します)。broadcasts、objects、frames、audio の各フローは、単一のコレクターでのみ使用できます。
  8. iOS の Player は @MainActor です — メインアクター上で構築し、操作してください。iOS の Publisher.stop() はエンコーダーのフラッシュを同期的に行います — メインスレッド以外から呼び出してください(Task.detached を使用します。Publisher は Sendable ではないため、Swift 6 の厳密な並行性チェックでは nonisolated(unsafe) を使用するか、Swift 5 モードのターゲットが必要です)。
  9. 1つのセッションが同じプレフィックスをパブリッシュしてサブスクライブすると、自分自身のブロードキャストも受信します — 報告されるパスはプレフィックス相対であることに注意して除外してください。サブスクライブのプレフィックスを取り除いたパスを、パブリッシュのパスと比較します。1つのプロセス内で2つの Session(パブリッシュ用とサブスクライブ用)を使うこともできます。セッションは一度しか使えないため、別々にしておくと、パブリッシュ側のトランスポート障害や破棄が再生に影響するのを防げます。
  10. レイテンシは実行中に調整できます:targetBuffering のデフォルトは 100 ms です。player.updateTargetLatency(_:) を使って再生中に調整できます。同じメディアトラックの2つ目のコンシューマー(別の Player や AudioDataStream)は、最初のサブスクライバーの上流バッファリングを再利用するため、個別にバッファリングを減らすことはできません。
  11. カスタムソースはパブリッシャーと同じクロックドメインを共有する必要があります。 Publisher は最初のトラックのフレームを基準にした1つのエポックを使って、すべてのトラックにタイムスタンプを付けます。そのため、独自のゼロ始まりのタイムラインを出力するソースはマイクと同期がずれたり、動画がはるか未来にスケジュールされたりします。iOS:ホストクロック上の PTS(CMClockGetTime(CMClockGetHostTimeClock()))を使用します。これは AVFoundation のキャプチャがすでに使用しているドメインです。Android:PCM には SystemClock.elapsedRealtimeNanos() / 1_000(MicrophoneCapture が使用する値)を使用し、エンコーダーサーフェスのプレゼンテーションタイムスタンプにも同じクロックを使用してください。

症状 → 原因

症状考えられる原因
ブロードキャストがまったく届かないリレー URL に名前空間パス(ローカルでは /anon)がありません。リレーが NotFound(code=13)を返すため、何も表示されません。または、セッションが接続状態になる前に subscribe() が実行されています
パブリッシュは「成功」するが、サブスクライバーにメディアが届かないキャプチャが開始されていません。publisher.start() はキャプチャを開始しません(ルール 3)
マイクの音声が聞こえないiOS:AVAudioSession が録音可能なカテゴリに設定されていません。Android:RECORD_AUDIO がなく、MicrophoneCapture.start() はエラーを出さずに何もせず終了します
Android:音声は再生されるが、映像が真っ黒サーフェスが設定されていないか、surfaceDestroyed 後の古いサーフェスが残っています。そこで null にし、isValid のサーフェスだけを再適用してください
iOS:音声は再生されるが、映像が真っ黒player.videoLayer がレイヤー階層に追加されていないか、サイズが設定されていません。階層に追加されて play() が実行されるまで描画されません
track.ready / playback.start が一度も発生しないplay() の後にサブスクライブしたか、(iOS)返された PlayerEventSubscription を保持していません
データトラックの送信が消えるpublisher.start() の前、または stop() の後に送信しています。どちらのプラットフォームでも何も通知されずに破棄されるため、パブリッシャーの状態に応じて送信してください
ネットワークが一時的に途切れた後、何も復旧しないセッションは一度きりです。Android では、要求していない Closed が失敗を示します。新しい Session を作成してください。ただし、Error(message) が表示されたら記録してください。問題の説明が記載されるのはこれだけです
リモートエラーに code=N だけが含まれるコードは moq_net::Error の判別値です。13 は NotFound、24 は Dropped(「プロデューサーが完了せずに破棄された」)、25 は Closed、26 は Lagged です。全一覧は moq-net の error.rs(moq-kit のチェックアウトでは vendor/moq/rs/moq-net/src/error.rs)にあります
iOS:配信を停止すると UI が引っかかるPublisher.stop() はエンコーダーを同期的にフラッシュします。メインスレッド以外で呼び出してください
パブリッシャー自身のブロードキャストがサブスクライバー一覧に表示される同じセッションで同じプレフィックスをパブリッシュおよびサブスクライブしています(ルール 9)。プレフィックスからの相対パスでフィルタリングしてください
カスタムソース:音声と映像がずれる、または音声の再生中に映像がフリーズするフレームのタイムスタンプが、パブリッシャーの他のトラックと同じクロックドメインにありません(ルール 11)
Android:play() が IllegalArgumentException をスローするデコードできない選択済みトラックに対する UnsupportedCodecException(サブクラス)です。先にこちらを捕捉してください
Android:画面キャプチャが始まった後、停止するmediaProjection フォアグラウンドサービスがまだ起動していなかったか、同意結果のコールバック以外で起動されました(Android 14 以降では拒否されます)
デバイスやエミュレーターからローカルリレーに接続できないlocalhost はデバイス自体を指します。Android エミュレーターでは 10.0.2.2 を、物理デバイスではホストの LAN IP を使用してください

デバッグ

リレー、サブスクリプション、パブリッシュの失敗は、通常、状態に反映される前にネイティブトランスポートのログに現れます。他の API を操作する前に、ログレベルを上げてください。

KitLogger.setNativeLogLevel("debug")   // "error"/"warn"/"info"/"debug"/"trace"; only the first call takes effect
NativeLogging.setLogLevel("debug")     // same levels; invalid values are ignored with a warning

Android のネイティブログは MoQNative タグの logcat に出力されますが、ネイティブライブラリが android-logcat cargo 機能(moq-ffi → moq-native → tracing-android)を有効にしてビルドされている場合に限ります。有効でない場合、tracing サブスクライバーは stderr に書き込みますが、Android はこれを破棄します。setLogLevel の呼び出しが成功した後も MoQNative の行が出ない場合、使用中のアーティファクトではこの機能が無効になっています。その場合は、セッション/パブリッシャーの状態とプレーヤーイベントを確認してください。

iOS では、サブシステム com.swmansion.MoQKit(カテゴリは session、transport、catalog、media、player、publish)の os_log 経由でもログが出力されます。Console.app でこのサブシステムをフィルタリングしてください。フレーム単位のドロップ/停止の詳細は、これらのログではなくプレーヤーの診断チャンネルに出力されます。

リレーに対するテスト

moq-kit のチェックアウトで(Rust と mise が必要です):mise run relay:run でローカルの moq-lite リレー http://localhost:4443/anon が起動します。mise run media:to-fmp4 で動画を CMAF fMP4 に変換し、mise run stream:file --input file.mp4 でそれをループ再生してリレーにブロードキャストします。実機からは localhost の代わりにマシンの LAN IP を使用し、Android エミュレーターからは 10.0.2.2 を使用してください(これはホストのループバックを指すエミュレーター固有のエイリアスです。エミュレーター自身の localhost はエミュレーター自体を指します。iOS シミュレーターはホストを共有します)。リレー URL には名前空間パス(ローカルでは /anon)を含める必要があります。パスが間違っているか抜けていると、ブロードキャストは表示されません(リレーの NotFound、code=13)。デモアプリ(examples/ios/demo/MoQDemo、examples/android/demo/MoQDemo)が標準的な統合例です。

リファレンス

タスクiOS(Swift)Android(Kotlin)
再生:セッション、検出、Player、レンダリング、イベント/統計の連携references/playback-ios.mdreferences/playback-android.md
パブリッシング:カメラ、マルチカメラ、マイク、プレビュー、エンコーダー設定、権限references/publishing-ios.mdreferences/publishing-android.md
画面共有references/screen-capture-ios.mdreferences/screen-capture-android.md
データトラック、圧縮メディアの生データ(MediaTrack)、デコード済み PCM(AudioDataStream)references/data-tracks-ios.mdreferences/data-tracks-android.md

コメント