Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BlacklistAwareVideoDecoderFactory #1204

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
package io.getstream.video.android.data.services.stream

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import io.getstream.video.android.model.User
import io.getstream.video.android.models.UserCredentials
import io.getstream.video.android.models.builtInCredentials
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.create
import retrofit2.http.GET
import retrofit2.http.Query

interface StreamService {
fun interface StreamService {
@GET("api/auth/create-token")
suspend fun getAuthData(
@Query("environment") environment: String,
Expand All @@ -41,6 +44,15 @@ interface StreamService {
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

val instance = retrofit.create<StreamService>()
private val serviceInstance = retrofit.create<StreamService>()

val instance = StreamService { environment, userId ->
User.builtInCredentials[userId]?.toAuthDataResponse()
?: serviceInstance.getAuthData(environment, userId)
}
}
}

private fun UserCredentials.toAuthDataResponse(): GetAuthDataResponse {
return GetAuthDataResponse(userId, apiKey, token)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ package io.getstream.video.android.models

import io.getstream.video.android.model.User

data class UserCredentials(val userId: String, val apiKey: String, val token: String)

public val User.Companion.builtInCredentials: Map<String, UserCredentials>
get() = mapOf()

public fun User.Companion.builtInUsers(): List<User> {
return listOf(
User(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class io/getstream/video/android/core/Call {
public final fun isLocalPin (Ljava/lang/String;)Z
public final fun isPinnedParticipant (Ljava/lang/String;)Z
public final fun isServerPin (Ljava/lang/String;)Z
public final fun isVideoEnabled ()Z
public final fun join (ZLio/getstream/video/android/core/CreateCallOptions;ZZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun join$default (Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/core/CreateCallOptions;ZZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun leave ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,10 @@ public class Call(
return state.ownCapabilities.value.containsAll(elements)
}

fun isVideoEnabled(): Boolean {
return state.settings.value?.video?.enabled ?: false
}

fun isAudioProcessingEnabled(): Boolean {
return clientImpl.isAudioProcessingEnabled()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import io.getstream.video.android.core.model.StreamPeerType
import kotlinx.coroutines.CoroutineScope
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.BlacklistAwareVideoDecoderFactory
import org.webrtc.EglBase
import org.webrtc.Logging
import org.webrtc.ManagedAudioProcessingFactory
Expand Down Expand Up @@ -101,9 +101,7 @@ public class StreamPeerConnectionFactory(
* Default video decoder factory used to unpack video from the remote tracks.
*/
private val videoDecoderFactory by lazy {
DefaultVideoDecoderFactory(
eglBase.eglBaseContext,
)
BlacklistAwareVideoDecoderFactory(eglBase.eglBaseContext)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.webrtc

import android.media.MediaCodecInfo
import android.media.MediaCodecList
import io.getstream.log.taggedLogger

internal class BlacklistAwareVideoDecoderFactory(
eglContext: EglBase.Context?,
) : VideoDecoderFactory {

private val logger by taggedLogger("BlacklistAwareVideoDecoderFactory")

private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.ALL_CODECS) }

/**
* Blacklist of codecs that are known to be buggy; we want to force software decoding for them.
*/
private val isHardwareDecoderBlacklisted: (MediaCodecInfo?) -> Boolean = {
it?.isExynosVP9() ?: false
}

private val allowedCodecPredicate: Predicate<MediaCodecInfo> = Predicate {
MediaCodecUtils.isHardwareAccelerated(it) || MediaCodecUtils.isSoftwareOnly(it)
}

private val hardwareVideoDecoderFactory: VideoDecoderFactory =
HardwareVideoDecoderFactory(eglContext)
private val softwareVideoDecoderFactory: VideoDecoderFactory = SoftwareVideoDecoderFactory()
private val platformSoftwareVideoDecoderFactory: VideoDecoderFactory =
PlatformSoftwareVideoDecoderFactory(eglContext)

override fun createDecoder(codecType: VideoCodecInfo): VideoDecoder? {
val type = VideoCodecMimeType.valueOf(codecType.getName())
val codec = findCodecForType(type)
logger.d { "[createDecoder] codecType: $codecType, codec: ${codec?.stringify()}" }

var softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType)
val hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType)
if (softwareDecoder == null) {
softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType)
}

if (isHardwareDecoderBlacklisted(codec)) {
logger.i { "[createDecoder] hardware decoder is blacklisted: ${codec?.stringify()}" }
return softwareDecoder
}

return if (hardwareDecoder != null && softwareDecoder != null) {
VideoDecoderFallback(softwareDecoder, hardwareDecoder)
} else {
hardwareDecoder ?: softwareDecoder
}
}

override fun getSupportedCodecs(): Array<VideoCodecInfo> {
val supportedCodecInfos = mutableSetOf<VideoCodecInfo>().apply {
addAll(softwareVideoDecoderFactory.supportedCodecs)
addAll(hardwareVideoDecoderFactory.supportedCodecs)
addAll(platformSoftwareVideoDecoderFactory.supportedCodecs)
}
logger.v { "[getSupportedCodecs] supportedCodecInfos: $supportedCodecInfos" }
return supportedCodecInfos.toTypedArray<VideoCodecInfo>()
}

private fun findCodecForType(type: VideoCodecMimeType): MediaCodecInfo? {
val codecInfos: List<MediaCodecInfo> = try {
mediaCodecList.codecInfos.filterNotNull().toList()
} catch (e: Throwable) {
logger.e(e) { "[findCodecForType] failed: $e" }
emptyList()
}
return codecInfos.firstOrNull {
!it.isEncoder && isSupportedCodec(it, type)
}
}

private fun isSupportedCodec(codec: MediaCodecInfo, type: VideoCodecMimeType): Boolean {
if (!MediaCodecUtils.codecSupportsType(codec, type)) {
return false
}
val colorFormat = MediaCodecUtils.selectColorFormat(
MediaCodecUtils.DECODER_COLOR_FORMATS,
codec.getCapabilitiesForType(type.mimeType()),
)
if (colorFormat == null) {
return false
}
return isCodecAllowed(codec)
}

private fun isCodecAllowed(info: MediaCodecInfo): Boolean {
return allowedCodecPredicate.test(info)
}
}

private fun MediaCodecInfo.isExynosVP9(): Boolean {
return !isEncoder && name.contains("exynos", ignoreCase = true) &&
name.contains("vp9", ignoreCase = true)
}

private fun MediaCodecInfo.stringify(): String {
return "MediaCodecInfo(" +
"name=$name, " +
"canonicalName=$canonicalName, " +
"isAlias=$isAlias, " +
"isVendor=$isVendor, " +
"isEncoder=$isEncoder, " +
"isHardwareAccelerated=$isHardwareAccelerated, " +
"isSoftwareOnly=$isSoftwareOnly, " +
"supportedTypes=${supportedTypes.joinToString()}" +
")"
}
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,9 @@ public abstract class StreamCallActivity : ComponentActivity() {

// Decision making
@StreamCallActivityDelicateApi
public open fun isVideoCall(call: Call): Boolean =
call.hasCapability(OwnCapability.SendVideo)
public open fun isVideoCall(call: Call): Boolean {
return call.hasCapability(OwnCapability.SendVideo) || call.isVideoEnabled()
}

// Picture in picture (for Video calls)
/**
Expand Down
Loading