While I am streaming move my app in background then I am getting this error
Maintainers usually reply within 3 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 12/100
- Issue type
- Bug
- Clarity
- Needs clarification
- Activity status
- Stale
- Domain
- audio-video-rtc, mobile
Research direction
The stack trace runs through GlStreamInterface.kt (draw and onFrameAvailable), MainRender.kt (updateFrame) and CameraRender.java (updateTexImage) in the RootEncoder library. Start by reading how the GL context is made current when the app moves to the background while a stream is running. Done would mean the crash no longer happens in that flow, and the fix is confirmed with a reproduction the issue does not yet include.
Written by the indexing model from the issue text.
Description
Issue 1
eglMakeCurrent failed
2025-04-17 13:36:20.333 5269-5364 GLConsumer com....streamaudioforegroundservice E [SurfaceTexture-1-5269-2] checkAndUpdateEglState: invalid current EGLContext
2025-04-17 13:36:20.351 5269-5364 AndroidRuntime com....streamaudioforegroundservice E FATAL EXCEPTION: pool-2-thread-1 (Ask Gemini)
Process: com.fsryan.mux.streamaudioforegroundservice, PID: 5269
java.lang.IllegalStateException: Unable to update texture contents (see logcat for details)
at android.graphics.SurfaceTexture.nativeUpdateTexImage(Native Method)
at android.graphics.SurfaceTexture.updateTexImage(SurfaceTexture.java:243)
at com.pedro.encoder.input.gl.render.CameraRender.updateTexImage(CameraRender.java:134)
at com.pedro.encoder.input.gl.render.MainRender.updateFrame(MainRender.kt:172)
at com.pedro.library.view.GlStreamInterface.draw(GlStreamInterface.kt:208)
at com.pedro.library.view.GlStreamInterface.onFrameAvailable$lambda$9(GlStreamInterface.kt:268)
at com.pedro.library.view.GlStreamInterface.$r8$lambda$BAtYl6ZVCZbXX960JSr5g3IQUyw(Unknown Source:0)
at com.pedro.library.view.GlStreamInterface
$$ExternalSyntheticLambda2.run(D8$$SyntheticClass:0)
package com.fsryan.mux.streamaudioforegroundservice
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.SurfaceHolder
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.fsryan.foregroundaudiostreamer.FSAudioStreamingService
import com.pedro.library.rtmp.RtmpStream
import com.pedro.library.view.OpenGlView
import android.content.ServiceConnection
import android.widget.ImageButton
import com.pedro.encoder.input.sources.video.Camera1Source
import com.pedro.encoder.input.sources.video.Camera2Source
// --- IMPLEMENT ServiceConnection, REMOVE ConnectChecker ---
class PreviewActivity : AppCompatActivity(), SurfaceHolder.Callback, ServiceConnection {
private lateinit var surfaceView: OpenGlView
val width = 640
val height = 480
val vBitrate = 1200 * 1000
// private var rotation = 0 // Likely not needed if service handles orientation
private val sampleRate = 44100
private val isStereo = false
private val aBitrate = 128 * 1000
// private var recordPath = "" // Remove if genericStream/recording removed
// --- Keep permission variables ---
private lateinit var permissionLauncher: ActivityResultLauncher<String>
private lateinit var permissionRationaleDialog: AlertDialog
// --- Keep track of service state ---
private var serviceBinder: FSAudioStreamingService.LocalBinder? = null // To hold the binder
private var serviceRtmpStream: RtmpStream? = null // To hold the stream ref from service
private var isBound = false
private var isSurfaceReady = false
private var activeStreamKey: String? = null // Key of the stream the service is running
// --- Use a distinct TAG ---
private val TAG = "PreviewActivity"
// --- MODIFIED BroadcastReceiver ---
private val streamEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
FSAudioStreamingService.ACTION_STREAM_EVENT -> {
val eventType =
intent.getIntExtra(FSAudioStreamingService.EXTRA_EVENT_TYPE, Int.MIN_VALUE)
Log.d(TAG, "Received stream event from Service: $eventType")
runOnUiThread { // Ensure UI updates happen on the main thread
when (eventType) {
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_CONNECTION_ERROR -> {
activeStreamKey = null // Assume error means no active stream
val errorMessage =
intent.getStringExtra(FSAudioStreamingService.EXTRA_EVENT_REASON)
Log.e(TAG, "Connection Error from Service: $errorMessage")
toast("Stream Connection Error: $errorMessage")
stopPreviewAndCleanupStreamRef() // Stop preview if it was running
// TODO: Update UI (e.g., enable start button, disable stop button)
}
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_AUTH_ERROR -> {
activeStreamKey = null
Log.e(TAG, "Auth Error from Service")
toast("Stream Auth Error")
stopPreviewAndCleanupStreamRef()
// TODO: Update UI
}
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_DISCONNECTED -> {
Log.w(
TAG,
"Stream Disconnected from Service. Key: $activeStreamKey"
)
toast("Stream Disconnected")
stopPreviewAndCleanupStreamRef() // Stop preview
activeStreamKey = null // Clear the key
// TODO: Update UI
// --- DELETED CALL to stopRtpCameraXStream ---
}
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_AUTH_SUCCESS -> {
Log.i(TAG, "Auth Success from Service")
// Maybe show temporary success message
}
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_CONNECTION_STARTED -> {
val url =
intent.getStringExtra(FSAudioStreamingService.EXTRA_EVENT_STREAM_URL)
// Store the key the service is using
activeStreamKey = url?.let { Uri.parse(it).lastPathSegment }
Log.i(
TAG,
"Connection Started by Service. StreamKey: $activeStreamKey"
)
toast("Streaming Started...")
// Try to get stream ref via binder and start preview
tryGetStreamAndStartPreview()
// TODO: Update UI (e.g., disable start button, enable stop button)
// --- DELETED CALL to startRtpCameraXStream ---
}
FSAudioStreamingService.EXTRA_EVENT_TYPE_VALUE_CONNECTION_SUCCESS -> {
Log.i(TAG, "Connection Success from Service")
// Maybe show temporary success message
}
else -> {
Log.w(TAG, "Unknown event type from Service: $eventType")
}
}
}
}
else -> {
Log.w(TAG, "Unknown intent action received: ${intent?.action}")
}
}
}
}
private lateinit var switchCameraButton: ImageButton
private lateinit var flashToggleIB: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preview)
switchCameraButton = findViewById(R.id.flipCameraIB)
flashToggleIB = findViewById(R.id.flashToggleIB)
surfaceView = findViewById(R.id.surfaceView)
surfaceView.holder.addCallback(this) // Keep for preview
// --- Keep Permission setup ---
permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
if (hasPermissions()) {
// setMainContent()
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
} else {
requestRequiredPermissions()
}
} else {
requestRequiredPermissions()
}
}
if (hasPermissions()) {
// setMainContent()
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
} else {
requestRequiredPermissions()
}
// --- Keep BroadcastReceiver registration ---
FSAudioStreamingService.registerStreamEventBroadcastReceiver(this, streamEventReceiver)
val startForegroundService = findViewById<Button>(R.id.startForegroundservice)
val startStream = findViewById<Button>(R.id.startStream)
val stopStream = findViewById<Button>(R.id.stopStream)
serviceRtmpStream?.getStreamClient()?.setReTries(10)
/* startForegroundService.setOnClickListener {
FSAudioStreamingService.startServiceForeground(this)
}
startStream.setOnClickListener {
if (!hasPermissions()) {
toast("Required permissions missing!")
requestRequiredPermissions()
return@setOnClickListener
}
Log.d(TAG, "Start Stream button clicked. Sending intent to service.")
// Tell the SERVICE to start streaming
sendBroadcast(
FSAudioStreamingService.createStartStreamIntent(
passthrough = "test",
// Let service create/manage the key, pass null initially
// Or pass 'activeStreamKey' if trying to resume a known stream
streamKey = null, // Usually null to let Mux create one via service
testMode = true,
audioOnly = false,
videoWidth = width,
videoHeight = height,
videoBitrate = vBitrate,
audioSampleRate = sampleRate,
audioIsStereo = isStereo,
audioBitrate = aBitrate
// videoFPS = 30 // Add if service/library requires it
)
)
// TODO: Update button states immediately (e.g., disable start)
}*/
//StartAuto
FSAudioStreamingService.startServiceForeground(this)
if (!hasPermissions()) {
toast("Required permissions missing!")
requestRequiredPermissions()
return
}
Log.d(TAG, "Start Stream button clicked. Sending intent to service.")
// Tell the SERVICE to start streaming
stopStream.setOnClickListener {
activeStreamKey?.let { key -> // Use the key tracked from service events
Log.d(TAG, "Stop Stream button clicked. Sending intent for key: $key")
// Tell the SERVICE to stop streaming
sendBroadcast(FSAudioStreamingService.createStopStreamIntent(key))
// Preview will be stopped via DISCONNECTED event or onStop
// TODO: Update button states immediately (e.g., disable stop)
} ?: Toast.makeText(this, "Not currently streaming (no active key)", Toast.LENGTH_SHORT)
.show()
}
switchCameraButton.setOnClickListener {
switchCamera()
}
flashToggleIB.setOnClickListener {
toggleFlash()
}
}
fun createStartStream() {
sendBroadcast(
FSAudioStreamingService.createStartStreamIntent(
passthrough = "test",
// Let service create/manage the key, pass null initially
// Or pass 'activeStreamKey' if trying to resume a known stream
streamKey = null, // Usually null to let Mux create one via service
testMode = true,
audioOnly = false,
videoWidth = width,
videoHeight = height,
videoBitrate = vBitrate,
audioSampleRate = sampleRate,
audioIsStereo = isStereo,
audioBitrate = aBitrate
, videoFPS = 30
// videoFPS = 30 // Add if service/library requires it
)
)
}
//for binding
override fun onStart() {
super.onStart()
// Bind to the service when the activity becomes visible
if (hasPermissions()) {
Log.d(TAG, "onStart: Binding to service...")
Intent(this, FSAudioStreamingService::class.java).also { intent ->
bindService(intent, this, Context.BIND_AUTO_CREATE)
}
} else {
Log.w(TAG, "onStart: Permissions missing, cannot bind yet.")
}
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop: Unbinding from service...")
stopPreviewAndCleanupStreamRef() // Stop preview before/during unbind
if (isBound) {
unbindService(this)
isBound = false
serviceBinder = null // Clear binder reference
serviceRtmpStream = null // Clear stream reference
}
}
override fun onDestroy() {
super.onDestroy()
// serviceRtmpStream?.release()
// --- Unregister receiver
try {
unregisterReceiver(streamEventReceiver)
/* activeStreamKey.let {key->
sendBroadcast(key?.let { FSAudioStreamingService.createStopStreamIntent(it) })
}*/
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Receiver already unregistered?", e)
}
Log.d(TAG, "onDestroy finished.")
}
// --- Keep SurfaceHolder.Callback methods for preview ---
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG, "Surface created.")
isSurfaceReady = true
// Try to start preview now that surface is ready (if service is bound)
tryGetStreamAndStartPreview()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d(TAG, "Surface changed: $width x $height")
// Update preview resolution if the stream object is available
serviceRtmpStream?.getGlInterface()?.setPreviewResolution(width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "Surface destroyed.")
isSurfaceReady = false
stopPreviewAndCleanupStreamRef() // Stop preview when surface is gone
}
// --- End SurfaceHolder.Callback ---
// --- ADD ServiceConnection methods ---
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.i(TAG, "Service Bound: $className")
// Cast the IBinder and get LocalBinder instance
serviceBinder = service as FSAudioStreamingService.LocalBinder
isBound = true
if (activeStreamKey == null && hasPermissions()) { // Check if not already streaming and permissions OK
Log.i(TAG, "Service connected, automatically starting stream...")
createStartStream() // Send the command to the service
} else {
Log.d(
TAG,
"Service connected, but stream already active (key: $activeStreamKey) or permissions missing. Not auto-starting."
)
}
// Now that we are bound, try to get the stream reference and start preview
// (if surface is also ready and we have an active stream key)
tryGetStreamAndStartPreview()
}
override fun onServiceDisconnected(arg0: ComponentName) {
// Called only on unexpected disconnect (e.g., service crashed)
Log.e(TAG, "Service unexpectedly disconnected: $arg0")
isBound = false
serviceBinder = null // Clear binder reference
serviceRtmpStream = null // Clear stream reference
activeStreamKey = null
runOnUiThread { toast("Streaming service disconnected unexpectedly") }
}
// --- End ServiceConnection methods ---
// --- Keep Permission methods ---
private fun hasPermissions(): Boolean {
return FSAudioStreamingService.remainingRequiredPermissions(this).isEmpty()
}
private fun requestRequiredPermissions() {
val permission =
FSAudioStreamingService.remainingRequiredPermissions(this).firstOrNull() ?: return
ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
.let { shouldShowRationale ->
if (shouldShowRationale) {
if (this::permissionRationaleDialog.isInitialized && permissionRationaleDialog.isShowing) {
permissionRationaleDialog.dismiss()
}
permissionRationaleDialog = AlertDialog.Builder(this)
.setTitle("Permission Required")
.setMessage("This app requires the $permission permission to function properly")
.setPositiveButton("OK") { _, _ ->
permissionLauncher.launch(permission)
}.setNegativeButton("Cancel") { _, _ ->
finish()
}.create()
} else {
permissionLauncher.launch(permission)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_STORAGE_PERMISSION -> { // Recording (Or general storage)
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
toast("Storage permission granted.")
} else {
toast("Storage permission denied.")
}
}
REQUEST_STORAGE_PERMISSION_PHOTO -> { // Taking Photo specific
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
toast("Storage permission granted. Try taking photo again.")
// You could try calling takePhoto() again automatically here if desired
// takePhoto()
} else {
toast("Storage permission denied. Cannot save photo.")
}
}
}
}
// --- ADD Helper methods for preview ---
private fun tryGetStreamAndStartPreview() {
if (!isBound || !isSurfaceReady || activeStreamKey == null || serviceBinder == null) {
Log.d(
TAG,
"Cannot start preview yet. Bound=$isBound, SurfaceReady=$isSurfaceReady, Key=$activeStreamKey, Binder=${serviceBinder != null}"
)
return
}
val key = activeStreamKey!! // Safe due to check above
Log.d(TAG, "Attempting to get stream via Binder for key: $key and start preview...")
val stream = serviceBinder?.getRtmpStream(key) // Use the stored binder
if (stream != null) {
serviceRtmpStream = stream // Store reference to the service's stream object
// stopPreviewAndCleanupStreamRef()
//added
if (!surfaceView.holder.surface.isValid) {
Log.w(TAG, "Surface became invalid before preview could start.")
return
}
if (!serviceRtmpStream!!.isOnPreview) {
Log.i(TAG, "Starting preview on SurfaceView using service's stream")
try {
// Tell the *service's* stream object to preview on *this Activity's* view
serviceRtmpStream!!.startPreview(surfaceView)
} catch (e: Exception) {
Log.e(TAG, "Error starting preview via service stream", e)
runOnUiThread { toast("Error starting preview: ${e.message}") }
serviceRtmpStream = null // Clear ref on error
}
} else {
Log.d(TAG, "Service stream for key $key is already on preview.")
// Ensure the existing preview is using our surfaceView? (May not be needed if library handles surface replacement)
}
} else {
Log.w(TAG, "Binder returned null stream for key: $key")
serviceRtmpStream = null // Ensure ref is null if not found
}
}
private fun stopPreviewAndCleanupStreamRef() {
val stream = serviceRtmpStream ?: return // Exit if no stream ref
// Use the stored serviceRtmpStream reference
if (stream.isOnPreview) {
Log.i(TAG, "Stopping preview on SurfaceView via service stream")
try {
stream.stopPreview()
} catch (e: Exception) {
Log.e(TAG, "Error stopping preview via service stream", e)
}
}
// Don't necessarily nullify serviceRtmpStream here, that happens on unbind/disconnect
// However, if the stream itself disconnects, we should probably clear it.
}
/*private fun stopPreviewAndCleanupStreamRef() {
// Use the stored serviceRtmpStream reference
if (serviceRtmpStream != null && serviceRtmpStream!!.isOnPreview) {
Log.i(TAG, "Stopping preview on SurfaceView via service stream")
try {
serviceRtmpStream!!.stopPreview()
} catch (e: Exception) {
Log.e(TAG, "Error stopping preview via service stream", e)
}
}
// Don't necessarily nullify serviceRtmpStream here, that happens on unbind/disconnect
// However, if the stream itself disconnects, we should probably clear it.
}*/
// --- Keep Companion Object IF permission constants are used, otherwise remove ---
companion object {
// Keep these only if requestStoragePermission is kept for some reason
private const val REQUEST_STORAGE_PERMISSION = 101
private const val REQUEST_STORAGE_PERMISSION_PHOTO = 102
}
//function
private fun switchCamera() {
if (!isBound || serviceRtmpStream == null) {
Log.w(TAG, "Cannot switch camera: Not bound or service stream reference is null.")
toast("Not connected to streaming service")
return
}
// Check if the stream is actually running (preview or streaming)
// because switching might only work when the camera source is active.
if (!serviceRtmpStream!!.isOnPreview && !serviceRtmpStream!!.isStreaming) {
Log.w(TAG, "Cannot switch camera: Stream is not previewing or streaming.")
toast("Stream not active")
return
}
try {
Log.i(TAG, "Attempting to switch camera via service stream object...")
when (val source = serviceRtmpStream?.videoSource) {
is Camera1Source -> source.switchCamera()
is Camera2Source -> source.switchCamera()
// is CameraXSource -> source.switchCamera()
}
// Note: You might not get immediate feedback if the switch was successful.
// The library handles the internal camera reopening.
toast("Switching camera...") // Give user feedback
} catch (e: Exception) {
// Catch potential errors (e.g., CameraInUseException if something goes wrong)
Log.e(TAG, "Error switching camera via service stream", e)
runOnUiThread { toast("Error switching camera: ${e.message}") }
}
}
fun toggleFlash() {
var isFlashNowOn = false // To store the final state
try {
val source = serviceRtmpStream?.videoSource // Get the current video source
// Determine the current state and the action needed
val currentlyEnabled: Boolean
val toggleAction: (Any) -> Unit // Lambda to hold the action
when (source) {
is Camera1Source -> {
currentlyEnabled = source.isLanternEnabled() // Check current state
Log.d(TAG, "Camera1Source: Flash currently enabled = $currentlyEnabled")
toggleAction = { src ->
if (currentlyEnabled) (src as Camera1Source).disableLantern()
else (src as Camera1Source).enableLantern()
}
}
is Camera2Source -> {
currentlyEnabled = source.isLanternEnabled() // Check current state
Log.d(TAG, "Camera2Source: Flash currently enabled = $currentlyEnabled")
toggleAction = { src ->
if (currentlyEnabled) (src as Camera2Source).disableLantern()
else (src as Camera2Source).enableLantern()
}
}
// is CameraXSource -> { /* Add CameraX specific logic if needed */ }
else -> {
Log.w(TAG, "Flash control not supported for this video source.")
toast("Flash not available")
return // Exit early
}
}
// --- Execute the toggle action ---
toggleAction(source)
Log.d(TAG, "Attempted to ${if (currentlyEnabled) "disable" else "enable"} flash.")
// --- Verify the new state ---
// It's slightly safer to re-check the state after the action
isFlashNowOn = when (source) {
is Camera1Source -> source.isLanternEnabled()
is Camera2Source -> source.isLanternEnabled()
else -> false // Should not happen if we got this far
}
Log.d(TAG, "Flash state verified after toggle: $isFlashNowOn")
// Update UI based on the NEW state
runOnUiThread { // Ensure UI updates happen on the main thread
toast(if (isFlashNowOn) "Flash ON" else "Flash OFF")
}
} catch (e: Exception) {
// Catch potential exceptions during enable/disable (e.g., camera disconnected)
Log.e(TAG, "Error toggling flash", e)
runOnUiThread {
toast("Error controlling flash")
// Reset UI to a known state (e.g., off)
}
}
}
//when photo is it will automatically on flash based on condition
fun autoFlash() {
}
}
FSAudioStreamingService
package com.fsryan.foregroundaudiostreamer
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import com.fsryan.mux.MuxAPIClient
import com.fsryan.mux.MuxLiveStream
import com.pedro.common.ConnectChecker
import com.pedro.library.rtmp.RtmpStream
import com.pedro.library.view.OpenGlView
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class FSAudioStreamingService: Service(), ConnectChecker {
internal data class NotificationConfig(
@DrawableRes val iconRes: Int,
val title: String,
val description: String
)
internal data class NotificationsConfig(
val channelId: String,
val notificationId: Int,
val activityToLaunch: String,
val idle: NotificationConfig,
val streaming: NotificationConfig
)
internal data class MuxConfig(
val muxTokenId: String,
val muxSecretKey: String
)
internal data class AudioStreamConfig(
val bitrate: Int,
val sampleRate: Int,
val isStereo: Boolean,
val useEchoCanceler: Boolean,
val useNoiseSuppressor: Boolean
)
internal data class VideoStreamConfig(
val width: Int,
val height: Int,
val fps: Int,
val bitrate: Int
)
internal data class StreamConfig(
val audio: AudioStreamConfig,
val video: VideoStreamConfig,
val isTestMode: Boolean,
val audioOnlyMode: Boolean
)
internal data class Config(
val notifications: NotificationsConfig,
val mux: MuxConfig,
val stream: StreamConfig
)
private var alreadyStarted: Boolean = false
private lateinit var config: Config
private lateinit var client: MuxAPIClient
private lateinit var notificationChannel: NotificationChannel
private val scope = CoroutineScope(Dispatchers.IO)
// must access on main thread
private val streamMap = mutableMapOf<String, RtmpStream>()
private val streamIntentReceiver: BroadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_START_STREAM -> handleStartStream(intent)
ACTION_STOP_STREAM -> handleStopStream(intent)
}
}
}
//Added the binder to bind the service
//binder implementation start
inner class LocalBinder : Binder() {
fun getRtmpStream(key: String): RtmpStream? {
Log.d("FSAudioStreamingServiceBinder", "Activity requesting stream for key: $key")
return streamMap[key]
}
// You could add other methods here if the Activity needs more interaction
// fun getServiceInstance(): FSAudioStreamingService = this@FSAudioStreamingService
}
private val binder = LocalBinder()
override fun onBind(intent: Intent): IBinder? {
Log.i("FSAudioStreamingService", "Service bound")
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
Log.i("FSAudioStreamingService", "Service unbound")
// Return true if you want onRebind to be called instead of onBind
// when clients re-bind (after onUnbind returned true).
// Typically return super.onUnbind() or false if you don't need onRebind.
return super.onUnbind(intent)
}
// binder implementation ends
override fun onCreate() {
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (alreadyStarted) {
return START_STICKY
}
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
alreadyStarted = true
config = createFSAudioStreamingServiceConfig()
startServiceInForeground()
registerStreamIntentReceiver()
client = createMuxClient()
return START_STICKY
}
override fun stopService(name: Intent?): Boolean {
unregisterStreamIntentReceiver()
stopForeground(STOP_FOREGROUND_REMOVE)
return super.stopService(name)
}
/* override fun onBind(intent: Intent): IBinder? {
// We don't provide binding, so return null
return null
}*/
override fun onDestroy() {
// TODO(ryan): clean up
}
override fun onAuthError() {
Log.w("FSAudioStreamingService", "Auth error")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_AUTH_ERROR)
})
notifyStreamingIdle()
}
override fun onAuthSuccess() {
Log.i("FSAudioStreamingService", "Auth success")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_AUTH_SUCCESS)
})
}
override fun onConnectionFailed(reason: String) {
Log.w("FSAudioStreamingService", "Connection failed: $reason")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_CONNECTION_ERROR)
putExtra(EXTRA_EVENT_REASON, reason)
})
// notifyStreamingIdle()
if (streamMap.isEmpty()) {
notifyStreamingIdle()
}
}
override fun onConnectionStarted(url: String) {
Log.i("FSAudioStreamingService", "Connection started: $url")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_CONNECTION_STARTED)
putExtra(EXTRA_EVENT_STREAM_URL, url)
})
notifyStreamingStarted()
}
override fun onConnectionSuccess() {
Log.i("FSAudioStreamingService", "Connection success")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_CONNECTION_SUCCESS)
})
}
override fun onDisconnect() {
Log.i("FSAudioStreamingService", "Disconnected")
sendBroadcast(Intent(ACTION_STREAM_EVENT).apply {
putExtra(EXTRA_EVENT_TYPE, EXTRA_EVENT_TYPE_VALUE_DISCONNECTED)
})
// notifyStreamingIdle()
if (streamMap.isEmpty()) {
notifyStreamingIdle()
}
}
private fun notifyStreamingIdle() {
val notification = config.notifications.idle.createNotification()
val nm = getSystemService(NotificationManager::class.java)
nm.notify(config.notifications.notificationId, notification)
}
private fun notifyStreamingStarted() {
val notification = config.notifications.streaming.createNotification()
val nm = getSystemService(NotificationManager::class.java)
nm.notify(config.notifications.notificationId, notification)
}
private fun NotificationConfig.createNotification(): Notification {
val pendingIntent = PendingIntentCompat.getActivity(
this@FSAudioStreamingService,
0,
Intent().apply {
val activityComponentName = ComponentName(
packageName,
config.notifications.activityToLaunch
)
setComponent(activityComponentName)
},
PendingIntent.FLAG_UPDATE_CURRENT,
true
)
return NotificationCompat.Builder(
this@FSAudioStreamingService,
config.notifications.channelId
).setSmallIcon(iconRes)
.setContentTitle(title)
.setContentText(description)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setSilent(true)
.build()
}
private fun registerStreamIntentReceiver() {
val startFilter = IntentFilter(ACTION_START_STREAM)
val stopFilter = IntentFilter(ACTION_STOP_STREAM)
ContextCompat.registerReceiver(
this,
streamIntentReceiver,
startFilter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
ContextCompat.registerReceiver(
this,
streamIntentReceiver,
stopFilter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
private fun unregisterStreamIntentReceiver() {
unregisterReceiver(streamIntentReceiver)
}
private fun startServiceInForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationChannel = NotificationChannel(
config.notifications.channelId,
config.notifications.idle.title,
NotificationManager.IMPORTANCE_DEFAULT
)
notificationChannel.description = config.notifications.idle.description
getSystemService(NotificationManager::class.java)
.createNotificationChannel(notificationChannel)
}
val notification = config.notifications.idle.createNotification()
ServiceCompat.startForeground(
this,
config.notifications.notificationId,
notification,
serviceType()
)
}
private fun serviceType(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
return type
}
return 0
}
private fun createMuxClient(): MuxAPIClient {
return MuxAPIClient(
httpClient = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(Json {
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
})
}
install(Logging) {
this.level = LogLevel.ALL
logger = object: Logger {
override fun log(message: String) {
Log.d("FSAudioStreamingService", message)
}
}
}
},
muxTokenId = config.mux.muxTokenId,
muxTokenSecret = config.mux.muxSecretKey
)
}
/*private fun handleStopStream(intent: Intent) {
intent.getStringExtra(EXTRA_STREAM_KEY)?.let { streamKey ->
streamMap[streamKey]?.stopStream()
?: Log.d("FSAudioStreamingService", "No stream found for key: $streamKey")
} ?: error("No stream key provided")
}*/
private fun handleStopStream(intent: Intent) {
val streamKey = intent.getStringExtra(EXTRA_STREAM_KEY)
if (streamKey == null) {
Log.e("FSAudioStreamingService", "Stop stream intent missing stream key")
// Maybe stop all streams? Or just log error.
return
}
Log.i("FSAudioStreamingService", "Handling stop stream for key: $streamKey")
// Get the stream from the map
val stream = streamMap[streamKey]
if (stream != null) {
// Stop the stream if it's running
if (stream.isStreaming) {
Log.d("FSAudioStreamingService", "Calling stopStream() for key: $streamKey")
stream.stopStream() // This might trigger onDisconnect eventually
}
// Optional: Stop preview if service was managing it (unlikely with Binder approach)
// if (stream.isOnPreview) {
// stream.stopPreview()
// }
// Remove the stream and release resources AFTER stopping
removeStreamFromMap(streamKey) // Use the helper function
} else {
Log.w("FSAudioStreamingService", "Stop command received, but no stream found for key: $streamKey")
// If no stream found, ensure we are idle if map becomes empty
if (`streamMap`.isEmpty()) {
notifyStreamingIdle()
}
}
}
private fun handleStartStream(intent: Intent) {
val passthrough = intent.getStringExtra(EXTRA_PASSTHROUGH)
val streamKey = intent.getStringExtra(EXTRA_STREAM_KEY)
val streamConfig = intent.createStreamConfig()
scope.launch {
startStream(
streamConfig = streamConfig,
passthrough = passthrough,
streamKey = streamKey
)
}
}
private suspend fun startStream(
streamConfig: StreamConfig,
passthrough: String?,
streamKey: String?,
) {
if (streamKey != null) {
accessStream(streamKey, createAsNecessary = true) { stream ->
performStartStream(streamConfig, streamKey, stream)
}
return
}
createStream(
passthrough = passthrough,
audioOnly = streamConfig.audioOnlyMode,
test = streamConfig.isTestMode
) { key, stream ->
performStartStream(streamConfig, key, stream)
}
}
private fun performStartStream(
streamConfig: StreamConfig,
key: String,
stream: RtmpStream
) {
val url = MuxLiveStream.rtmpStreamUrl(key)
stream.prepare(streamConfig)
stream.startStream(url)
}
private suspend fun createStream(
passthrough: String?,
audioOnly: Boolean,
test: Boolean,
perform: (String, RtmpStream) -> Unit
) {
val output = client.createStream(
request = MuxLiveStream.Create(
audioOnly = audioOnly,
passthrough = passthrough,
test = test
)
)
val data = output.getOrNull()?.data
?: error("Failed to create stream; ${output.exceptionOrNull()}")
Log.i("FSAudioStreamingService", "Created stream with key: $data")
addNewStream(data.streamKey) { stream ->
perform(data.streamKey, stream)
}
}
/* private suspend fun addNewStream(
key: String,
perform: (RtmpStream) -> Unit
) = withContext(Dispatchers.Main) {
val current = streamMap[key]
if (current != null) {
Log.w("FSAudioStreamingService", "Stream already exists for key: $key")
perform(current)
} else {
val stream = RtmpStream(this@FSAudioStreamingService, this@FSAudioStreamingService)
streamMap[key] = stream
perform(stream)
}
}*/
private suspend fun addNewStream(
key: String,
perform: (RtmpStream) -> Unit
) { // Removed withContext(Dispatchers.Main) as ConcurrentHashMap is thread-safe
val current = streamMap[key]
if (current != null) {
Log.w("FSAudioStreamingService", "Stream already exists for key: $key")
// Decide if you want to re-use or error out
perform(current) // Perform on existing stream
} else {
// Create the new stream instance
// The RtmpStream constructor needs Context and ConnectChecker
Log.d("FSAudioStreamingService", "Creating new RtmpStream for key: $key")
val stream = RtmpStream(this@FSAudioStreamingService, this@FSAudioStreamingService)
// Since RtmpStream itself might interact with UI elements or require
// main thread for camera/audio initialization, it *might* be safer
// to ensure its creation and initial prepare happens on Main.
// However, let's try without for now and see if pedro.library handles it.
// If camera errors occur, wrap stream creation and perform() in withContext(Dispatchers.Main)
streamMap[key] = stream // Add to thread-safe map
perform(stream) // Perform on the newly added stream
}
}
/* private suspend fun accessStream(
key: String,
createAsNecessary: Boolean = false,
perform: (RtmpStream) -> Unit
) {
withContext(Dispatchers.Main) {
val stream = when (createAsNecessary) {
true -> streamMap.getOrPut(key) {
RtmpStream(this@FSAudioStreamingService, this@FSAudioStreamingService)
}
false -> streamMap[key]
}
stream?.let(perform) ?: error("No stream found for key: $key")
}
}*/
private suspend fun accessStream(
key: String,
createAsNecessary: Boolean = false,
perform: (RtmpStream) -> Unit
) { // Removed withContext(Dispatchers.Main)
val stream = if (createAsNecessary) {
streamMap.computeIfAbsent(key) { // Atomic operation
Log.d("FSAudioStreamingService", "Creating new RtmpStream via accessStream for key: $it")
RtmpStream(this@FSAudioStreamingService, this@FSAudioStreamingService)
}
} else {
streamMap[key]
}
if (stream != null) {
perform(stream)
} else {
Log.e("FSAudioStreamingService", "No stream found for key: $key in accessStream")
// Consider throwing an error or handling appropriately
// error("No stream found for key: $key")
}
}
private fun removeStreamFromMap(key: String) {
Log.d("FSAudioStreamingService", "Attempting to remove stream for key: $key")
val removedStream = streamMap.remove(key) // Remove from thread-safe map
if (removedStream != null) {
Log.i("FSAudioStreamingService", "Stream removed for key: $key. Releasing resources.")
// Ensure resources are released (stopPreview if needed, release GL, etc.)
// It's safer to call release on the stream object itself
if (removedStream.isOnPreview) {
try { removedStream.stopPreview() } catch (e: Exception) { Log.e("FSAudioStreamingService", "Error stopping preview during remove", e)}
}
try { removedStream.release() } catch (e: Exception) { Log.e("FSAudioStreamingService", "Error releasing stream during remove", e)}
// Check if service should go idle
if (streamMap.isEmpty()) {
Log.i("FSAudioStreamingService", "Last stream removed, setting notification to idle.")
notifyStreamingIdle()
}
} else {
Log.w("FSAudioStreamingService", "Attempted to remove stream, but key not found: $key")
}
}
private fun RtmpStream.prepare(streamConfig: StreamConfig) {
if (isStreaming || isRecording) {
stopStream()
}
prepareAudio(
bitrate = streamConfig.audio.bitrate,
sampleRate = streamConfig.audio.sampleRate,
isStereo = streamConfig.audio.isStereo,
echoCanceler = streamConfig.audio.useEchoCanceler,
noiseSuppressor = streamConfig.audio.useNoiseSuppressor
)
prepareVideo(
width = streamConfig.video.width,
height = streamConfig.video.height,
fps = streamConfig.video.fps,
bitrate = streamConfig.video.bitrate
)
}
private fun Intent.createStreamConfig(): StreamConfig {
val audioBitrate = getIntExtra(EXTRA_AUDIO_BITRATE, config.stream.audio.bitrate)
val audioSampleRate = getIntExtra(EXTRA_AUDIO_SAMPLE_RATE, config.stream.audio.sampleRate)
val audioIsStereo = getBooleanExtra(EXTRA_AUDIO_IS_STEREO, config.stream.audio.isStereo)
val audioUseEchoCanceler = getBooleanExtra(EXTRA_AUDIO_USE_ECHO_CANCELER, config.stream.audio.useEchoCanceler)
val audioUseNoiseSuppressor = getBooleanExtra(EXTRA_AUDIO_USE_NOISE_SUPPRESSOR, config.stream.audio.useNoiseSuppressor)
val videoWidth = getIntExtra(EXTRA_VIDEO_WIDTH, config.stream.video.width)
val videoHeight = getIntExtra(EXTRA_VIDEO_HEIGHT, config.stream.video.height)
val videoFPS = getIntExtra(EXTRA_VIDEO_FPS, config.stream.video.fps)
val videoBitrate = getIntExtra(EXTRA_VIDEO_BITRATE, config.stream.video.bitrate)
val isTest = getBooleanExtra(EXTRA_IS_TEST, config.stream.isTestMode)
val audioOnly = getBooleanExtra(EXTRA_AUDIO_ONLY, config.stream.audioOnlyMode)
return StreamConfig(
audio = AudioStreamConfig(
bitrate = audioBitrate,
sampleRate = audioSampleRate,
isStereo = audioIsStereo,
useEchoCanceler = audioUseEchoCanceler,
useNoiseSuppressor = audioUseNoiseSuppressor
),
video = VideoStreamConfig(
width = videoWidth,
height = videoHeight,
fps = videoFPS,
bitrate = videoBitrate
),
isTestMode = isTest,
audioOnlyMode = audioOnly
)
}
companion object {
const val ACTION_START_STREAM = "com.fsryan.mux.foregroundaudiostreamer.START_STREAM"
const val ACTION_STOP_STREAM = "com.fsryan.mux.foregroundaudiostreamer.STOP_STREAM"
const val ACTION_STREAM_EVENT = "com.fsryan.mux.foregroundaudiostreamer.STREAM_EVENT"
// extras for stream indentification
const val EXTRA_PASSTHROUGH = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_PASSTHROUGH"
const val EXTRA_STREAM_KEY = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_STREAM_KEY"
const val EXTRA_IS_TEST = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_IS_TEST"
const val EXTRA_AUDIO_ONLY = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_ONLY"
// extras for audio stream config
const val EXTRA_AUDIO_BITRATE = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_BITRATE"
const val EXTRA_AUDIO_SAMPLE_RATE = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_SAMPLE_RATE"
const val EXTRA_AUDIO_IS_STEREO = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_IS_STEREO"
const val EXTRA_AUDIO_USE_ECHO_CANCELER = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_USE_ECHO_CANCELER"
const val EXTRA_AUDIO_USE_NOISE_SUPPRESSOR = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_AUDIO_USE_NOISE_SUPPRESSOR"
// extras for video stream config
const val EXTRA_VIDEO_WIDTH = "com.fsryan.mux.foregroundaudiostream.EXTRA_VIDEO_WIDTH"
const val EXTRA_VIDEO_HEIGHT = "com.fsryan.mux.foregroundaudiostream.EXTRA_VIDEO_HEIGHT"
const val EXTRA_VIDEO_FPS = "com.fsryan.mux.foregroundaudiostream.EXTRA_VIDEO_FPS"
const val EXTRA_VIDEO_BITRATE = "com.fsryan.mux.foregroundaudiostream.EXTRA_VIDEO_BITRATE"
const val EXTRA_EVENT_TYPE = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_EVENT_TYPE"
const val EXTRA_EVENT_REASON = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_EVENT_REASON"
const val EXTRA_EVENT_STREAM_URL = "com.fsryan.mux.foregroundaudiostreamer.EXTRA_EVENT_STREAM_URL"
const val EXTRA_EVENT_TYPE_VALUE_CONNECTION_ERROR = -2
const val EXTRA_EVENT_TYPE_VALUE_AUTH_ERROR = -1
const val EXTRA_EVENT_TYPE_VALUE_DISCONNECTED = 0
const val EXTRA_EVENT_TYPE_VALUE_AUTH_SUCCESS = 1
const val EXTRA_EVENT_TYPE_VALUE_CONNECTION_STARTED = 2
const val EXTRA_EVENT_TYPE_VALUE_CONNECTION_SUCCESS = 3
fun createStartServiceIntent(context: Context): Intent {
return Intent(context, FSAudioStreamingService::class.java)
}
fun startServiceForeground(context: Context) {
val intent = createStartServiceIntent(context)
ContextCompat.startForegroundService(context, intent)
}
fun createStartStreamIntent(
passthrough: String?,
streamKey: String?,
audioBitrate: Int? = null,
audioSampleRate: Int? = null,
audioIsStereo: Boolean? = null,
audioUseEchoCanceler: Boolean? = null,
audioUseNoiseSuppressor: Boolean? = null,
videoWidth: Int? = null,
videoHeight: Int? = null,
videoFPS: Int? = null,
videoBitrate: Int? = null,
testMode: Boolean? = null,
audioOnly: Boolean? = null
): Intent {
return Intent(ACTION_START_STREAM).apply {
passthrough?.let { putExtra(EXTRA_PASSTHROUGH, it) }
streamKey?.let { putExtra(EXTRA_STREAM_KEY, it) }
audioBitrate?.let { putExtra(EXTRA_AUDIO_BITRATE, it) }
audioSampleRate?.let { putExtra(EXTRA_AUDIO_SAMPLE_RATE, it) }
audioIsStereo?.let { putExtra(EXTRA_AUDIO_IS_STEREO, it) }
audioUseEchoCanceler?.let { putExtra(EXTRA_AUDIO_USE_ECHO_CANCELER, it) }
audioUseNoiseSuppressor?.let { putExtra(EXTRA_AUDIO_USE_NOISE_SUPPRESSOR, it) }
videoWidth?.let { putExtra(EXTRA_VIDEO_WIDTH, it) }
videoHeight?.let { putExtra(EXTRA_VIDEO_HEIGHT, it) }
videoFPS?.let { putExtra(EXTRA_VIDEO_FPS, it) }
videoBitrate?.let { putExtra(EXTRA_VIDEO_BITRATE, it) }
testMode?.let { putExtra(EXTRA_IS_TEST, it) }
audioOnly?.let { putExtra(EXTRA_AUDIO_ONLY, it) }
}
}
fun createStopStreamIntent(streamKey: String): Intent {
return Intent(ACTION_STOP_STREAM).apply {
putExtra(EXTRA_STREAM_KEY, streamKey)
}
}
fun remainingRequiredPermissions(context: Context): List<String> = mutableListOf<String>().apply {
if (!permissionIsGranted(context, Manifest.permission.RECORD_AUDIO)) {
add(Manifest.permission.RECORD_AUDIO)
}
if (!permissionIsGranted(context, Manifest.permission.CAMERA)) {
add(Manifest.permission.CAMERA)
}
if (!permissionIsGranted(context, Manifest.permission.ACCESS_FINE_LOCATION)) {
add(Manifest.permission.ACCESS_FINE_LOCATION)
}
if (!permissionIsGranted(context, Manifest.permission.ACCESS_FINE_LOCATION)) {
add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // 33
if (!permissionIsGranted(context, Manifest.permission.POST_NOTIFICATIONS)) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // 34
if (!permissionIsGranted(context, Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)) {
add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
}
if (!permissionIsGranted(context, Manifest.permission.FOREGROUND_SERVICE_CAMERA)) {
add(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
}
if (!permissionIsGranted(context, Manifest.permission.FOREGROUND_SERVICE_LOCATION)) {
add(Manifest.permission.FOREGROUND_SERVICE_LOCATION)
}
}
}
fun registerStreamEventBroadcastReceiver(context: Context, receiver: BroadcastReceiver) {
val filter = IntentFilter(ACTION_STREAM_EVENT)
ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
private fun permissionIsGranted(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PermissionChecker.PERMISSION_GRANTED
}
}
}
This issue comes when i am streaming and automatically this come after some time and streaming stops
issue 2
tivity com....streamaudioforegroundservice D Received stream event from Service: -2
2025-04-17 13:45:45.161 7214-7214 PreviewActivity com....streamaudioforegroundservice E Connection Error from Service: Error send packet, SocketTimeoutException
2025-04-17 13:45:45.173 7214-7289 RtmpSender com....streamaudioforegroundservice I Video frame discarded
2025-04-17 13:45:45.184 7214-7262 RtmpSender com....streamaudioforegroundservice E send error: (Ask Gemini)
java.net.SocketTimeoutException
at io.ktor.network.sockets.CIOWriterKt$attachForWritingDirectImpl$1$1$timeout$1.invokeSuspend(CIOWriter.kt:91)
at io.ktor.network.sockets.CIOWriterKt$attachForWritingDirectImpl$1$1$timeout$1.invoke(Unknown Source:8)
at io.ktor.network.sockets.CIOWriterKt$attachForWritingDirectImpl$1$1$timeout$1.invoke(Unknown Source:2)
at io.ktor.network.util.Timeout$initTimeoutJob$1.invokeSuspend(Utils.kt:60)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
- Dominant language
- Kotlin
- Stars
- 3k
- Forks
- 946
- Avg merge
- 5d 1h
- Merged PRs (30d)
- 6
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pedroSG94/RootEncoder
-
Scheme-less stream URLs throw NullPointerException instead of URISyntaxExceptionPossibly taken @blihio claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
pedroSG94/RootEncoder#2218 ·
Maintainers usually reply within 3 days
-
Nullable GIF input causes NullPointerException in GifStreamObject.loadPossibly taken @blihio claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
pedroSG94/RootEncoder#2216 ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
pedroSG94/RootEncoder#2213 ·
Maintainers usually reply within 3 days
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
pedroSG94/RootEncoder#2207 · 1 reaction ·
Maintainers usually reply within 3 days
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
pedroSG94/RootEncoder#2203 ·
Maintainers usually reply within 3 days
All issues in pedroSG94/RootEncoder
Similar issues
-
binding:web core docs p3
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
RunanywhereAI/runanywhere-sdks#1123 ·
Maintainers usually reply within 1 day
-
StepExamBenchmarkTest runs a loop and prints a timing but asserts nothingPossibly taken A pull request linked to this issue is open or already merged. Openjules
Difficulty 1/5 Under an hour Newbie friendliness 82/100
open-learning-exchange/myplanet#18199 · 1 comment ·
Maintainers usually reply within 4 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
auth0/Auth0.Android#1095 · 1 comment ·
Maintainers usually reply within 1 day
-
Test: PlantDetailPlantEditLockTest silent-apply case fails depending on the calendar date (seasonal amplitude)Possibly taken @LocNgu claimed this today. Openbug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
LocNgu/YAPT-Yet-Another-Plant-Tracker#907 ·
Maintainers usually reply within 1 day
-
Estimation - 2 (S) Type:bug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Maintainers usually reply within 2 days