0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-11

はじめに

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

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


ゴール

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

image.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 用にビルド

# ソース取得
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:

adb push mediamtx /data/local/tmp/

設定ファイル(mediamtx.yml)のうちデフォルトから変えた部分:

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)も同じ場所に配置。

adb push mediamtx.yml /data/local/tmp/

Step 2: mediamtx 起動

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 ライブラリを使用。

Gradle依存関係:

implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.2.5'

AndroidManifest.xmlに必要なパーミッション:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
アプリコードの例:

MainActivity.kt (importとかは省略)

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

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

Android の IP アドレスを確認:

設定 -> ネットワークとインターネット -> インターネット -> 自分が接続しているWi-Fi -> IPアドレス
例:192.168.0.102

mediamtx.yml の設定確認:

# 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: EPERMINTERNET パーミッション不足 or セキュリティ制限
  • HLS にアクセスできない:hlsAddress のバインドを 0.0.0.0 もしくはIPを書かずにportだけで書く
  • streamが見つからない:RTMPで送信している stream path が正しいか確認(例:/live/streamの部分はアプリのソースのrtmpCamera.startStreamで指定しているのでPCからアクセスするときも合わせる必要がある。)

まとめ

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

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?