Newer
Older
* Copyright (C) 2004-2023 Savoir-faire Linux Inc.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.jami.services
import io.reactivex.rxjava3.core.*
import io.reactivex.rxjava3.core.ObservableOnSubscribe
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import net.jami.model.Call.CallStatus
import net.jami.daemon.IntVect
import net.jami.daemon.UintVect
import net.jami.daemon.JamiService
import net.jami.daemon.StringMap
import net.jami.model.Conference
import net.jami.utils.Log
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.jvm.Synchronized
abstract class HardwareService(
private val mExecutor: ScheduledExecutorService,
val mPreferenceService: PreferencesService,
protected val mUiScheduler: Scheduler
) {
data class VideoEvent (
val start: Boolean = false,
val started: Boolean = false,
val w: Int = 0,
val h: Int = 0,
val rot: Int = 0
)
enum class AudioOutputType {
INTERNAL, WIRED, SPEAKERS, BLUETOOTH
data class AudioOutput(val type: AudioOutputType, val outputName: String? = null, val outputId: String? = null)
data class AudioState(val output: AudioOutput, val availableOutputs: List<AudioOutput> = emptyList())
protected val videoEvents: Subject<VideoEvent> = PublishSubject.create()
protected val cameraEvents: Subject<VideoEvent> = PublishSubject.create()
protected val bluetoothEvents: Subject<BluetoothEvent> = PublishSubject.create()
protected val audioStateSubject: Subject<AudioState> = BehaviorSubject.createDefault(STATE_INTERNAL)
protected val connectivityEvents: Subject<Boolean> = BehaviorSubject.create()
fun getVideoEvents(): Observable<VideoEvent> = videoEvents
fun getCameraEvents(): Observable<VideoEvent> = cameraEvents
fun getBluetoothEvents(): Observable<BluetoothEvent> = bluetoothEvents
abstract fun getAudioState(conf: Conference): Observable<AudioState>
val audioState: Observable<AudioState>
get() = audioStateSubject
val connectivityState: Observable<Boolean>
get() = connectivityEvents
abstract fun initVideo(): Completable
abstract val isVideoAvailable: Boolean
abstract fun updateAudioState(conf: Conference?, call: Call, incomingCall: Boolean, isOngoingVideo: Boolean)
abstract fun toggleSpeakerphone(conf: Conference, checked: Boolean)
abstract fun abandonAudioFocus()
abstract fun decodingStarted(id: String, shmPath: String, width: Int, height: Int, isMixer: Boolean)
abstract fun decodingStopped(id: String, shmPath: String, isMixer: Boolean)
abstract fun hasInput(id: String): Boolean
abstract fun getCameraInfo(camId: String, formats: IntVect, sizes: UintVect, rates: UintVect)
abstract fun setParameters(camId: String, format: Int, width: Int, height: Int, rate: Int)
abstract fun startCapture(camId: String?)
abstract fun stopCapture(camId: String)
abstract fun hasMicrophone(): Boolean
abstract fun requestKeyFrame(camId: String)
abstract fun setBitrate(camId: String, bitrate: Int)
abstract fun addVideoSurface(id: String, holder: Any)
abstract fun updateVideoSurfaceId(currentId: String, newId: String)
abstract fun removeVideoSurface(id: String)
abstract fun addPreviewVideoSurface(holder: Any, conference: Conference?)
abstract fun updatePreviewVideoSurface(conference: Conference)
abstract fun removePreviewVideoSurface()
abstract fun changeCamera(setDefaultCamera: Boolean = false): String?
abstract fun setPreviewSettings()
abstract fun hasCamera(): Boolean
abstract val maxResolutions: Observable<Pair<Int?, Int?>>
abstract val isPreviewFromFrontCamera: Boolean
abstract fun shouldPlaySpeaker(): Boolean
abstract fun unregisterCameraDetectionCallback()
abstract fun startMediaHandler(mediaHandlerId: String?)
abstract fun stopMediaHandler()
abstract fun setPendingScreenShareProjection(screenCaptureSession: Any?)
fun connectivityChanged(isConnected: Boolean) {
Log.i(TAG, "connectivityChange() $isConnected")
connectivityEvents.onNext(isConnected)
mExecutor.execute { JamiService.connectivityChanged() }
}
fun switchInput(accountId:String, callId: String, uri: String) {
mExecutor.execute { JamiService.switchInput(accountId, callId, uri) }
fun setPreviewSettings(cameraMaps: Map<String, StringMap>) {
mExecutor.execute {
Log.i(TAG, "applySettings() thread running...")
for ((key, value) in cameraMaps) {
JamiService.applySettings(key, value)
}
}
}
fun startVideo(inputId: String, surface: Any, width: Int, height: Int): Long {
Log.i(TAG, "startVideo $inputId ${width}x$height")
val inputWindow = JamiService.acquireNativeWindow(surface)
if (inputWindow != 0L) {
JamiService.setNativeWindowGeometry(inputWindow, width, height)
JamiService.registerVideoCallback(inputId, inputWindow)
}
return inputWindow
}
fun stopVideo(inputId: String, inputWindow: Long) {
Log.i(TAG, "stopVideo $inputId $inputWindow")
if (inputWindow != 0L) {
JamiService.unregisterVideoCallback(inputId, inputWindow)
JamiService.releaseNativeWindow(inputWindow)
abstract fun connectSink(id: String, windowId: Long): Observable<Pair<Int, Int>>
abstract fun getSinkSize(id: String): Single<Pair<Int, Int>>
abstract fun setDeviceOrientation(rotation: Int)
protected abstract fun videoDevices(): List<String>
private var logs: Observable<List<String>>? = null
private var logEmitter: Emitter<String>? = null
@get:Synchronized
val isLogging: Boolean
get() = logs != null
@Synchronized
fun startLogs(): Observable<List<String>> {
return logs ?: Observable.create { emitter: ObservableEmitter<String> ->
// Queue the service call on daemon executor to be sure it has been initialized.
mExecutor.execute { JamiService.monitor(true) }
emitter.setCancellable {
synchronized(this@HardwareService) {
mExecutor.execute { JamiService.monitor(false) }
logEmitter = null
logs = null
}
}
.buffer(500, TimeUnit.MILLISECONDS)
.replay()
.autoConnect()
.apply { logs = this }
}
@Synchronized
fun stopLogs() {
logEmitter?.let { emitter ->
Log.w(TAG, "stopLogs JamiService.monitor(false)")
JamiService.monitor(false)
emitter.onComplete()
logEmitter = null
logs = null
}
}
fun logMessage(message: String) {
if (message.isNotEmpty())
logEmitter?.onNext(message)
}
companion object {
private val TAG = HardwareService::class.simpleName!!
val OUTPUT_SPEAKERS = AudioOutput(AudioOutputType.SPEAKERS)
val OUTPUT_INTERNAL = AudioOutput(AudioOutputType.INTERNAL)
val OUTPUT_WIRED = AudioOutput(AudioOutputType.WIRED)
val OUTPUT_BLUETOOTH = AudioOutput(AudioOutputType.BLUETOOTH)
val STATE_INTERNAL = AudioState(OUTPUT_INTERNAL)