Android端末上でmediamtxを動かして、カメラ映像を同一LANのPCからHLS視聴するまで

## はじめに

この記事では、Go製のメディアサーバー「mediamtx」をAndroid端末上でネイティブに動作させ、スマホのカメラ映像をRTMPでmediamtxに入力し、同一LAN内のPCからHLSで視聴できるようにするまでの手順を紹介します。

特別なroot化などは不要（ただし一部操作にはadbが必要）で、スマホ1台＋PCで完結します。

---

## ゴール

- Android端末で `mediamtx` を実行
- カメラ映像をRTMPでmediamtxへ送信
- PCブラウザからHLS視聴できる状態に

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325308/a8bc2c70-4a64-430d-8679-e750a6680319.png)

---

## 前提環境

- Android端末（arm64, Android 10以上）（私はPixel 7aを使いました）
- Android端末とPCは同一LAN上
- WSLにAndroid NDK, adb がインストール済み
- WSLにGo環境（Go 1.23 ~ 推奨）がインストール済み
- Android Studio + Kotlin プロジェクト

---

## Step 1: mediamtx を Android 用にビルド

```bash
# ソース取得
git clone https://github.com/bluenviron/mediamtx
cd mediamtx

git checkout v1.11.0

go clean -modcache

go generate ./...

# Android用クロスビルド
export GOOS=android
export GOARCH=arm64
export CC=$HOME/Android/Sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang

go build .
```

ビルド成功後、バイナリをスマホにpush：
```bash
adb push mediamtx /data/local/tmp/
```

設定ファイル（`mediamtx.yml`）のうちデフォルトから変えた部分：
```yaml
paths:
  # example:
  # my_camera:
  #   source: rtsp://my_camera
  all:
    source: publisher
  # Settings under path "all_others" are applied to all paths that
  # do not match another entry.
  # all_others:
```

設定ファイル（`mediamtx.yml`）も同じ場所に配置。
```bash
adb push mediamtx.yml /data/local/tmp/
```

---

## Step 2: mediamtx 起動

```bash
adb shell
cd /data/local/tmp
chmod +x ./mediamtx
./mediamtx
```

確認：ログに `HLS server listening on :8888` などが表示されればOK。

---

## Step 3: Androidアプリからカメラ映像をRTMPで送信

PedroSG94さんの [rtmp-rtsp-stream-client-java](https://github.com/pedroSG94/rtmp-rtsp-stream-client-java) ライブラリを使用。

Gradle依存関係：
```gradle
implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.2.5'
```

AndroidManifest.xmlに必要なパーミッション：
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
```

<details><summary>アプリコードの例：</summary>

MainActivity.kt (importとかは省略)
```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 100)
        }
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 101)
        }
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.INTERNET), 102)
        }

        val openGlView = findViewById<OpenGlView>(R.id.opengl_view)
        val connectCheckerRtmp = object : ConnectCheckerRtmp {
            override fun onConnectionSuccessRtmp() {
                Log.i("RTMP", "接続成功！")
            }

            override fun onConnectionFailedRtmp(reason: String) {
                Log.e("RTMP", "接続失敗: $reason")
            }

            override fun onConnectionStartedRtmp(rtmpUrl: String) {
                Log.i("RTMP", "接続開始されました: $rtmpUrl")
            }

            override fun onDisconnectRtmp() {
                Log.i("RTMP", "切断されました")
            }

            override fun onNewBitrateRtmp(bitrate: Long) {
                Log.i("RTMP", "new bitrate = $bitrate")
            }

            override fun onAuthErrorRtmp() {
                Log.e("RTMP", "認証エラー")
            }

            override fun onAuthSuccessRtmp() {
                Log.i("RTMP", "認証成功")
            }
        }
        val rtmpCamera = RtmpCamera2(openGlView, connectCheckerRtmp)
        openGlView.post {
            if (rtmpCamera.prepareAudio() && rtmpCamera.prepareVideo()) {
                rtmpCamera.startStream("rtmp://127.0.0.1:1935/live/camera")
            }
        }
    }
}
```

activity_main.xml:
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.pedro.rtplibrary.view.OpenGlView
        android:id="@+id/opengl_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

</details>

---

## Step 4: 同一LANのPCからHLSアクセス

### Android の IP アドレスを確認：
設定 -> ネットワークとインターネット -> インターネット -> 自分が接続しているWi-Fi -> IPアドレス
例：`192.168.0.102`

### mediamtx.yml の設定確認：
```yaml
# Enable reading streams with the HLS protocol.
hls: yes
# Address of the HLS listener.
hlsAddress: :8888  # = 0.0.0.0:8888（全インターフェースバインド）
```

### PC からアクセス：
```
http://192.168.0.102:8888/live/camera/
```

これでPCのVLCやブラウザから映像が再生できれば成功！

---

## トラブルシューティング

- `java.net.SocketException: socket failed: EPERM`：`INTERNET` パーミッション不足 or セキュリティ制限
- HLS にアクセスできない：`hlsAddress` のバインドを `0.0.0.0` もしくはIPを書かずにportだけで書く
- streamが見つからない：RTMPで送信している stream path が正しいか確認（例：`/live/stream`の部分はアプリのソースのrtmpCamera.startStreamで指定しているのでPCからアクセスするときも合わせる必要がある。）

---

## まとめ

Android端末単体でmediamtxを動かし、PCからHLS視聴する構成は軽量で開発検証にも便利です。
今後は録画・多端末配信・WebView再生などの応用も可能なはずです。

フィードバック歓迎です！