Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

While I am streaming move my app in background then I am getting this error

Open
#1,807 3 comments 0 reactions 0 assignees View on GitHub

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
Tech stack
android, kotlin

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from pedroSG94/RootEncoder

All issues in pedroSG94/RootEncoder

Similar issues

More Kotlin issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.