Add SRT implementation with mediamtx
parent
f95c8d7620
commit
6fb843a27a
252
SRT-implementation-with-mediamtx.md
Normal file
252
SRT-implementation-with-mediamtx.md
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
### Overview
|
||||||
|
Add dual-pipeline recording & streaming (MediaMuxer ↔ local + SRT → MediaMTX) to your existing Android project using StreamPack.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 1. Update Gradle Dependencies
|
||||||
|
**File:** `app/build.gradle.kts`
|
||||||
|
```kotlin
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.urkob.wittrail_android"
|
||||||
|
compileSdk = 34
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.urkob.wittrail_android"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 2
|
||||||
|
versionName = "2.0"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose & Lifecycle
|
||||||
|
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-service:2.7.0")
|
||||||
|
|
||||||
|
// CameraX
|
||||||
|
implementation("androidx.camera:camera-camera2:1.2.0")
|
||||||
|
implementation("androidx.camera:camera-lifecycle:1.2.0")
|
||||||
|
implementation("androidx.camera:camera-view:1.2.0")
|
||||||
|
|
||||||
|
// StreamPack for dual-record & SRT
|
||||||
|
implementation("io.github.thibaultbee.streampack:streampack-core:3.0.0-RC")
|
||||||
|
implementation("io.github.thibaultbee.streampack:streampack-ui:3.0.0-RC")
|
||||||
|
implementation("io.github.thibaultbee.streampack:streampack-extension-srt:3.0.0-RC")
|
||||||
|
|
||||||
|
// Other
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2. AndroidManifest Modifications
|
||||||
|
**File:** `app/src/main/AndroidManifest.xml`
|
||||||
|
```xml
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.urkob.wittrail_android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:theme="@style/Theme.WittrailAndroid">
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.StreamRecordingService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="camera|microphone"
|
||||||
|
android:enabled="true" />
|
||||||
|
|
||||||
|
<activity android:name=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 3. Create StreamRecordingService
|
||||||
|
**File:** `app/src/main/java/com/urkob/wittrail_android/service/StreamRecordingService.kt`
|
||||||
|
```kotlin
|
||||||
|
package com.urkob.wittrail_android.service
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
|
import io.github.thibaultbee.streampack.Streamer
|
||||||
|
import io.github.thibaultbee.streampack.extension.srt.UriMediaDescriptor
|
||||||
|
import io.github.thibaultbee.streampack.ext.cameraDualStreamer
|
||||||
|
import io.github.thibaultbee.streampack.ext.setAudioConfig
|
||||||
|
import io.github.thibaultbee.streampack.ext.setVideoConfig
|
||||||
|
import io.github.thibaultbee.streampack.media.AudioConfig
|
||||||
|
import io.github.thibaultbee.streampack.media.VideoConfig
|
||||||
|
import android.util.Size
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class StreamRecordingService : LifecycleService() {
|
||||||
|
private lateinit var streamer: Streamer
|
||||||
|
private val channelId = "StreamRecordingChannel"
|
||||||
|
private val notificationId = 1001
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createNotificationChannel()
|
||||||
|
startForeground(notificationId, buildNotification("Initializing..."))
|
||||||
|
|
||||||
|
streamer = cameraDualStreamer(context = this)
|
||||||
|
streamer.setVideoConfig(
|
||||||
|
VideoConfig(
|
||||||
|
resolution = Size(1280, 720),
|
||||||
|
fps = 30,
|
||||||
|
startBitrate = 2_000_000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
streamer.setAudioConfig(
|
||||||
|
AudioConfig(
|
||||||
|
sampleRate = 44100,
|
||||||
|
channelConfig = android.media.AudioFormat.CHANNEL_IN_STEREO,
|
||||||
|
startBitrate = 128_000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
streamer.startPreview()
|
||||||
|
|
||||||
|
val localFile = File(getExternalFilesDir(null),
|
||||||
|
"session_${System.currentTimeMillis()}.mp4")
|
||||||
|
streamer.startRecord(localFile.absolutePath)
|
||||||
|
|
||||||
|
val srtUrl = "srt://YOUR_MTX_HOST:8890/camera?passphrase=mySecretKey&pbkeylen=16"
|
||||||
|
streamer.startStream(UriMediaDescriptor(srtUrl))
|
||||||
|
|
||||||
|
updateNotification("Streaming to MTX server...")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
streamer.stopStream()
|
||||||
|
streamer.stopRecord()
|
||||||
|
streamer.release()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
super.onBind(intent)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
channelId,
|
||||||
|
"Stream Recording",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
getSystemService(NotificationManager::class.java)
|
||||||
|
.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(content: String): Notification {
|
||||||
|
return NotificationCompat.Builder(this, channelId)
|
||||||
|
.setContentTitle("Wittrail Recording")
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 4. MainActivity: Control Buttons
|
||||||
|
**File:** `app/src/main/java/com/urkob/wittrail_android/MainActivity.kt`
|
||||||
|
```kotlin
|
||||||
|
package com.urkob.wittrail_android
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.urkob.wittrail_android.service.StreamRecordingService
|
||||||
|
import com.urkob.wittrail_android.ui.theme.WittrailAndroidTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
WittrailAndroidTheme {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isRecording = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
Intent(context, StreamRecordingService::class.java).also { intent ->
|
||||||
|
if (!isRecording.value) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.stopService(intent)
|
||||||
|
}
|
||||||
|
isRecording.value = !isRecording.value
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(if (!isRecording.value) "Start Recording" else "Stop Recording")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 5. Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Android_Client
|
||||||
|
A[CameraX + Mic] -->|DualStreamer| B[StreamPack]
|
||||||
|
B --> C[Local MP4 via MediaMuxer]
|
||||||
|
B --> D[SRT → MediaMTX]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server
|
||||||
|
D --> E[MediaMTX SRT Ingest]
|
||||||
|
E --> F[runOnPublish → FFmpeg MP4]
|
||||||
|
F --> G[Storage + Audit]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### Next Steps
|
||||||
|
1. **Replace** `YOUR_MTX_HOST` with your server’s address in `srtUrl`.
|
||||||
|
2. **Grant permissions** at runtime for camera, mic, and storage.
|
||||||
|
3. **Test** via `Start Recording` button and verify local MP4 plus server-side recordings.
|
||||||
|
4. **Enhance**: compute SHA‑256 on the MP4 chunks or on file completion and log for audit.
|
||||||
|
5. **Extend**: add error callbacks (`streamer.setStreamErrorCallback`) to update UI.
|
||||||
|
|
||||||
|
This setup gives you a production-ready, dual-pipeline Android client—local backup + secure, low-latency SRT streaming to your MediaMTX server.
|
Loading…
x
Reference in New Issue
Block a user