davidliu
Committed by GitHub

Fix memory leaks (#170)

* Dispose of EglBase

* Dispose of SurfaceTextureHelper when disposing track, and dispose local tracks during cleanup

* Unneeded Surface creation

* Fix tests
... ... @@ -18,6 +18,7 @@ import javax.inject.Singleton
JsonFormatModule::class,
OverridesModule::class,
AudioHandlerModule::class,
MemoryModule::class,
]
)
internal interface LiveKitComponent {
... ...
package io.livekit.android.dagger
import dagger.Module
import dagger.Provides
import io.livekit.android.memory.CloseableManager
import javax.inject.Singleton
@Module
object MemoryModule {
@Singleton
@Provides
fun closeableManager() = CloseableManager()
}
\ No newline at end of file
... ...
... ... @@ -6,6 +6,7 @@ import androidx.annotation.Nullable
import dagger.Module
import dagger.Provides
import io.livekit.android.LiveKit
import io.livekit.android.memory.CloseableManager
import io.livekit.android.util.LKLog
import io.livekit.android.util.LoggingLevel
import io.livekit.android.webrtc.SimulcastVideoEncoderFactoryWrapper
... ... @@ -108,8 +109,11 @@ object RTCModule {
@Provides
@Singleton
fun eglBase(): EglBase {
return EglBase.create()
fun eglBase(@Singleton memoryManager: CloseableManager): EglBase {
val eglBase = EglBase.create()
memoryManager.registerResource(eglBase) { eglBase.release() }
return eglBase
}
@Provides
... ...
package io.livekit.android.memory
import java.io.Closeable
/**
* @hide
*/
class CloseableManager : Closeable {
private var isClosed = false
private val resources = mutableMapOf<Any, Closeable>()
@Synchronized
fun registerResource(key: Any, closer: Closeable) {
if (isClosed) {
closer.close()
return
} else {
resources[key] = closer
}
}
@Synchronized
fun registerClosable(closable: Closeable) {
if (isClosed) {
closable.close()
return
} else {
resources[closable] = closable
}
}
@Synchronized
fun unregisterResource(key: Any): Closeable? {
return resources.remove(key)
}
@Synchronized
override fun close() {
isClosed = true
resources.values.forEach { it.close() }
resources.clear()
}
}
\ No newline at end of file
... ...
... ... @@ -15,7 +15,6 @@ import android.graphics.Matrix
import android.graphics.SurfaceTexture
import android.os.Looper
import android.util.AttributeSet
import android.view.Surface
import android.view.SurfaceHolder
import android.view.TextureView
import android.view.View
... ... @@ -283,7 +282,7 @@ open class TextureViewRenderer : TextureView, SurfaceHolder.Callback, TextureVie
// TextureView.SurfaceTextureListener implementation
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, i: Int, i1: Int) {
ThreadUtils.checkIsOnMainThread()
eglRenderer.createEglSurface(Surface(surfaceTexture))
eglRenderer.createEglSurface(surfaceTexture)
surfaceHeight = 0
surfaceWidth = surfaceHeight
updateSurfaceSize()
... ...
... ... @@ -20,6 +20,7 @@ import io.livekit.android.events.BroadcastEventBus
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.events.RoomEvent
import io.livekit.android.events.collect
import io.livekit.android.memory.CloseableManager
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.*
... ... @@ -33,6 +34,7 @@ import livekit.LivekitModels
import livekit.LivekitRtc
import org.webrtc.*
import javax.inject.Named
import javax.inject.Singleton
class Room
@AssistedInject
... ... @@ -47,6 +49,8 @@ constructor(
@Named(InjectionNames.DISPATCHER_IO)
private val ioDispatcher: CoroutineDispatcher,
val audioHandler: AudioHandler,
@Singleton
private val closeableManager: CloseableManager,
) : RTCEngine.Listener, ParticipantListener {
private lateinit var coroutineScope: CoroutineScope
... ... @@ -250,6 +254,16 @@ constructor(
}
/**
* Release all resources held by this object.
*
* Once called, this room object must not be used to connect to a server and a new one
* must be created.
*/
fun release() {
closeableManager.close()
}
/**
* @suppress
*/
override fun onJoinResponse(response: LivekitRtc.JoinResponse) {
... ...
... ... @@ -568,6 +568,7 @@ internal constructor(
if (track != null) {
track.stop()
unpublishTrack(track)
track.dispose()
}
}
}
... ...
... ... @@ -8,6 +8,7 @@ import androidx.core.content.ContextCompat
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.memory.CloseableManager
import io.livekit.android.room.DefaultsManager
import io.livekit.android.room.track.video.Camera1CapturerWithSize
import io.livekit.android.room.track.video.Camera2CapturerWithSize
... ... @@ -56,6 +57,8 @@ constructor(
private val sender: RtpSender?
get() = transceiver?.sender
private val closeableManager = CloseableManager()
open fun startCapture() {
capturer.startCapture(
options.captureParams.width,
... ... @@ -73,6 +76,12 @@ constructor(
super.stop()
}
override fun dispose() {
super.dispose()
capturer.dispose()
closeableManager.close()
}
fun setDeviceId(deviceId: String) {
restartTrack(options.copy(deviceId = deviceId))
}
... ... @@ -132,14 +141,6 @@ constructor(
* Restart a track with new options.
*/
fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) {
val newTrack = createTrack(
peerConnectionFactory,
context,
name,
options,
eglBase,
trackFactory
)
val oldCapturer = capturer
val oldSource = source
... ... @@ -152,6 +153,18 @@ constructor(
// sender owns rtcTrack, so it'll take care of disposing it.
oldRtcTrack.setEnabled(false)
val oldCloseable = closeableManager.unregisterResource(oldRtcTrack)
oldCloseable?.close()
val newTrack = createTrack(
peerConnectionFactory,
context,
name,
options,
eglBase,
trackFactory
)
// migrate video sinks to the new track
for (sink in sinks) {
oldRtcTrack.removeSink(sink)
... ... @@ -185,24 +198,28 @@ constructor(
name: String,
capturer: VideoCapturer,
options: LocalVideoTrackOptions = LocalVideoTrackOptions(),
rootEglBase: EglBase,
trackFactory: Factory
): LocalVideoTrack {
val source = peerConnectionFactory.createVideoSource(false)
val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
capturer.initialize(
SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext),
surfaceTextureHelper,
context,
source.capturerObserver
)
val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
return trackFactory.create(
val track = trackFactory.create(
capturer = capturer,
source = source,
options = options,
name = name,
rtcTrack = track
rtcTrack = rtcTrack
)
track.closeableManager.registerResource(rtcTrack) { surfaceTextureHelper.dispose() }
return track
}
internal fun createTrack(
peerConnectionFactory: PeerConnectionFactory,
... ... @@ -221,20 +238,25 @@ constructor(
val source = peerConnectionFactory.createVideoSource(options.isScreencast)
val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO()
val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
capturer.initialize(
SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext),
surfaceTextureHelper,
context,
source.capturerObserver
)
val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
return trackFactory.create(
val track = trackFactory.create(
capturer = capturer,
source = source,
options = newOptions,
name = name,
rtcTrack = track
rtcTrack = rtcTrack
)
track.closeableManager.registerResource(rtcTrack) { surfaceTextureHelper.dispose() }
return track
}
private fun createCameraEnumerator(context: Context): CameraEnumerator {
... ...
... ... @@ -5,6 +5,7 @@ import dagger.BindsInstance
import dagger.Component
import io.livekit.android.dagger.JsonFormatModule
import io.livekit.android.dagger.LiveKitComponent
import io.livekit.android.dagger.MemoryModule
import io.livekit.android.mock.MockWebSocketFactory
import io.livekit.android.room.RTCEngine
import javax.inject.Singleton
... ... @@ -17,6 +18,7 @@ import javax.inject.Singleton
TestWebModule::class,
TestAudioHandlerModule::class,
JsonFormatModule::class,
MemoryModule::class,
]
)
internal interface TestLiveKitComponent : LiveKitComponent {
... ...
... ... @@ -11,6 +11,7 @@ import io.livekit.android.events.EventCollector
import io.livekit.android.events.EventListenable
import io.livekit.android.events.ParticipantEvent
import io.livekit.android.events.RoomEvent
import io.livekit.android.memory.CloseableManager
import io.livekit.android.mock.*
import io.livekit.android.room.participant.LocalParticipant
import kotlinx.coroutines.ExperimentalCoroutinesApi
... ... @@ -74,6 +75,7 @@ class RoomTest {
defaultDispatcher = coroutineRule.dispatcher,
ioDispatcher = coroutineRule.dispatcher,
audioHandler = NoAudioHandler(),
closeableManager = CloseableManager()
)
}
... ...
... ... @@ -215,6 +215,7 @@ class CallViewModel(
override fun onCleared() {
super.onCleared()
room.disconnect()
room.release()
}
fun setMicEnabled(enabled: Boolean) {
... ...