はじめに
この記事では、Go製のメディアサーバー「mediamtx」をAndroid端末上でネイティブに動作させ、スマホのカメラ映像をRTMPでmediamtxに入力し、同一LAN内のPCからHLSで視聴できるようにするまでの手順を紹介します。
特別なroot化などは不要(ただし一部操作にはadbが必要)で、スマホ1台+PCで完結します。
ゴール
- Android端末で
mediamtx
を実行 - カメラ映像をRTMPでmediamtxへ送信
- PCブラウザからHLS視聴できる状態に
前提環境
- 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: 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再生などの応用も可能なはずです。
フィードバック歓迎です!