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