diff --git a/core/build.gradle b/core/build.gradle index 2437cf494..9bae34945 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -14,10 +14,15 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation "androidx.core:core-ktx:${androidxCoreVersion}" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - implementation 'androidx.test:rules:1.5.0' + implementation 'androidx.camera:camera-viewfinder:1.4.0-alpha04' + implementation 'com.google.guava:guava:31.0.1-jre' + + testImplementation 'androidx.test:rules:1.5.0' testImplementation 'junit:junit:4.13.2' - testImplementation "io.mockk:mockk:1.12.2" + testImplementation 'io.mockk:mockk:1.12.2' + + androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/extensions/SizeExtensions.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/extensions/SizeExtensions.kt index 75f9b9170..653fc2bf8 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/extensions/SizeExtensions.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/extensions/SizeExtensions.kt @@ -46,4 +46,10 @@ val Size.isPortrait: Boolean * Check if the size is in landscape orientation. */ val Size.isLandscape: Boolean - get() = !isPortrait \ No newline at end of file + get() = !isPortrait + +/** + * Find the closest size to the given size in a list of sizes. + */ +fun List.closestTo(size: Size): Size = + this.minBy { abs((it.width * it.height) - (size.width * size.height)) } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/streamers/bases/BaseCameraStreamer.kt b/core/src/main/java/io/github/thibaultbee/streampack/streamers/bases/BaseCameraStreamer.kt index 8c53b38bb..249a7c0e4 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/streamers/bases/BaseCameraStreamer.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/streamers/bases/BaseCameraStreamer.kt @@ -30,7 +30,6 @@ import io.github.thibaultbee.streampack.listeners.OnErrorListener import io.github.thibaultbee.streampack.streamers.helpers.CameraStreamerConfigurationHelper import io.github.thibaultbee.streampack.streamers.interfaces.ICameraStreamer import io.github.thibaultbee.streampack.streamers.settings.BaseCameraStreamerSettings -import io.github.thibaultbee.streampack.views.AutoFitSurfaceView import io.github.thibaultbee.streampack.views.PreviewView import kotlinx.coroutines.runBlocking @@ -89,7 +88,7 @@ open class BaseCameraStreamer( * * Inside, it launches both camera and microphone capture. * - * @param previewSurface Where to display camera capture. Could be a [Surface] from a [PreviewView], an [AutoFitSurfaceView], a [SurfaceView] or a [TextureView]. + * @param previewSurface Where to display camera capture. Could be a [Surface] from a [SurfaceView] or a [TextureView]. * @param cameraId camera id (get camera id list from [Context.cameraList]) * * @throws [StreamPackError] if audio or video capture couldn't be launch diff --git a/core/src/main/java/io/github/thibaultbee/streampack/views/AutoFitSurfaceView.kt b/core/src/main/java/io/github/thibaultbee/streampack/views/AutoFitSurfaceView.kt index d39cb48d6..9a26cac7e 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/views/AutoFitSurfaceView.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/views/AutoFitSurfaceView.kt @@ -26,6 +26,7 @@ import kotlin.math.roundToInt * A [SurfaceView] that can be adjusted to a specified aspect ratio and * performs center-crop transformation of input frames. */ +@Deprecated("Use PreviewView instead.", ReplaceWith("PreviewView")) open class AutoFitSurfaceView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, diff --git a/core/src/main/java/io/github/thibaultbee/streampack/views/CameraSizes.kt b/core/src/main/java/io/github/thibaultbee/streampack/views/CameraSizes.kt index 4d78270c4..bbac6dfdd 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/views/CameraSizes.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/views/CameraSizes.kt @@ -16,31 +16,9 @@ */ package io.github.thibaultbee.streampack.views -import android.graphics.Point import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.params.StreamConfigurationMap import android.util.Size -import android.view.Display -import kotlin.math.max -import kotlin.math.min - -/** Helper class used to pre-compute shortest and longest sides of a [Size] */ -class SmartSize(width: Int, height: Int) { - var size = Size(width, height) - var long = max(size.width, size.height) - var short = min(size.width, size.height) - override fun toString() = "SmartSize(${long}x${short})" -} - -/** Standard High Definition size for pictures and video */ -val SIZE_1080P: SmartSize = SmartSize(1920, 1080) - -/** Returns a [SmartSize] object for the given [Display] */ -fun getDisplaySmartSize(display: Display): SmartSize { - val outPoint = Point() - display.getRealSize(outPoint) - return SmartSize(outPoint.x, outPoint.y) -} +import io.github.thibaultbee.streampack.internal.utils.extensions.closestTo /** * Returns the largest available PREVIEW size. For more information, see: @@ -48,33 +26,20 @@ fun getDisplaySmartSize(display: Display): SmartSize { * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap */ fun getPreviewOutputSize( - display: Display, characteristics: CameraCharacteristics, + targetSize: Size, targetClass: Class, - format: Int? = null ): Size { - - // Find which is smaller: screen or 1080p - val screenSize = getDisplaySmartSize(display) - val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short - val maxSize = if (hdScreen) SIZE_1080P else screenSize - - // If image format is provided, use it to determine supported sizes; else use target class - val config = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP - )!! - if (format == null) - assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) - else - assert(config.isOutputSupportedFor(format)) - val allSizes = if (format == null) - config.getOutputSizes(targetClass) else config.getOutputSizes(format) + val allSizes = + characteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!.getOutputSizes( + targetClass + ).toList() // Get available sizes and sort them by area from largest to smallest val validSizes = allSizes .sortedWith(compareBy { it.height * it.width }) - .map { SmartSize(it.width, it.height) }.reversed() + .map { Size(it.width, it.height) }.reversed() // Then, get the largest output size that is smaller or equal than our max size - return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size -} \ No newline at end of file + return validSizes.closestTo(targetSize) +} diff --git a/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt b/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt index 560aee9bc..c76101c5e 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt @@ -21,13 +21,23 @@ import android.content.pm.PackageManager import android.graphics.PointF import android.graphics.Rect import android.util.AttributeSet +import android.util.Size import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener +import android.view.Surface import android.view.SurfaceHolder import android.view.ViewConfiguration +import android.view.ViewGroup import android.widget.FrameLayout +import androidx.camera.viewfinder.CameraViewfinder +import androidx.camera.viewfinder.CameraViewfinder.ScaleType +import androidx.camera.viewfinder.ViewfinderSurfaceRequest +import androidx.camera.viewfinder.populateFromCharacteristics import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.common.util.concurrent.FutureCallback +import com.google.common.util.concurrent.Futures import io.github.thibaultbee.streampack.R import io.github.thibaultbee.streampack.logger.Logger import io.github.thibaultbee.streampack.streamers.interfaces.ICameraStreamer @@ -37,36 +47,74 @@ import io.github.thibaultbee.streampack.utils.frontCameraList import io.github.thibaultbee.streampack.utils.getCameraCharacteristics /** - * A [FrameLayout] containing a [AutoFitSurfaceView] that manages [ICameraStreamer] preview. + * A [FrameLayout] containing a preview for the [ICameraStreamer]. + * + * It handles the display, the aspect ratio and the scaling of the preview. + * * In the case, you are using it, do not call [ICameraStreamer.startPreview] or - * [ICameraStreamer.stopPreview] on application side. + * [ICameraStreamer.stopPreview] on application side. It will be handled by the [PreviewView]. * - * The [Manifest.permission.CAMERA] permission must be granted before using this class.s + * The [Manifest.permission.CAMERA] permission must be granted before using this view. */ class PreviewView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : FrameLayout(context, attrs, defStyle) { - private val surfaceView = AutoFitSurfaceView(context) - private val cameraFacingDirection: FacingDirection + private val cameraViewFinder = CameraViewfinder(context, attrs, defStyle) + private var surfaceRequest: ViewfinderSurfaceRequest? = null + + private val cameraFacingDirection: CameraFacingDirection private val defaultCameraId: String? + private var isPreviewing = false + + /** + * Enables zoom on pinch gesture. + */ var enableZoomOnPinch: Boolean - var enableTapToFocus: Boolean - private var touchUpEvent: MotionEvent? = null + /** + * Enables tap to focus. + */ + var enableTapToFocus: Boolean + /** + * Sets the [ICameraStreamer] to preview. + * Once set, the [PreviewView] will try to start the preview. + * + * Only one [ICameraStreamer] can be set. + */ var streamer: ICameraStreamer? = null /** - * Set the [ICameraStreamer] to use. + * Sets the [ICameraStreamer] to preview. * - * @param value the [ICameraStreamer] to use + * @param value the [ICameraStreamer] to preview */ set(value) { - streamer?.stopPreview() - field = value - startPreviewIfReady() + post { + stopPreviewInternal() + field = value + startPreviewInternal(size) + } + } + + /** + * The position of the [PreviewView] within its container. + */ + var position: Position + get() = getPosition(cameraViewFinder.scaleType) + set(value) { + cameraViewFinder.scaleType = getScaleType(scaleMode, value) + } + + /** + * The scale mode of the [PreviewView] within its container. + */ + var scaleMode: ScaleMode + get() = getScaleMode(cameraViewFinder.scaleType) + set(value) { + cameraViewFinder.scaleType = getScaleType(value, position) } /** @@ -74,6 +122,8 @@ class PreviewView @JvmOverloads constructor( */ var listener: Listener? = null + private var touchUpEvent: MotionEvent? = null + private val pinchGesture = ScaleGestureDetector( context, PinchToZoomOnScaleGestureListener() @@ -83,16 +133,16 @@ class PreviewView @JvmOverloads constructor( val a = context.obtainStyledAttributes(attrs, R.styleable.PreviewView) try { - cameraFacingDirection = FacingDirection.fromValue( - a.getString(R.styleable.PreviewView_cameraFacingDirection) - ?: DEFAULT_CAMERA_FACING.value + cameraFacingDirection = CameraFacingDirection.entryOf( + a.getInt(R.styleable.PreviewView_cameraFacingDirection, DEFAULT_CAMERA_FACING.value) ) + defaultCameraId = when (cameraFacingDirection) { - FacingDirection.FRONT -> { + CameraFacingDirection.FRONT -> { context.frontCameraList.firstOrNull() } - FacingDirection.BACK -> { + CameraFacingDirection.BACK -> { context.backCameraList.firstOrNull() } } @@ -101,14 +151,41 @@ class PreviewView @JvmOverloads constructor( a.getBoolean(R.styleable.PreviewView_enableZoomOnPinch, true) enableTapToFocus = a.getBoolean(R.styleable.PreviewView_enableTapToFocus, true) + + scaleMode = ScaleMode.entryOf( + a.getInt( + R.styleable.PreviewView_scaleMode, + ScaleMode.FILL.value + ) + ) + position = Position.entryOf( + a.getInt( + R.styleable.PreviewView_position, + Position.CENTER.value + ) + ) + } finally { a.recycle() } - surfaceView.holder.addCallback(StreamerHolderCallback()) - addView(surfaceView) + addView( + cameraViewFinder, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + startPreviewIfReady(size, true) } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopPreview() + } override fun onTouchEvent(event: MotionEvent): Boolean { if (streamer == null) { @@ -136,7 +213,7 @@ class PreviewView @JvmOverloads constructor( } override fun performClick(): Boolean { - streamer?.let { it -> + streamer?.let { if (enableTapToFocus) { // mTouchUpEvent == null means it's an accessibility click. Focus at the center instead. val x = touchUpEvent?.x ?: (width / 2f) @@ -153,65 +230,189 @@ class PreviewView @JvmOverloads constructor( return super.performClick() } - private fun startPreviewIfReady(shouldFailSilently: Boolean = false) { - if (display != null) { - streamer?.let { - try { - val camera = defaultCameraId ?: it.camera - Logger.i(TAG, "Starting on camera: $camera") - - // Selects appropriate preview size - val previewSize = getPreviewOutputSize( - this.display, - context.getCameraCharacteristics(camera), - SurfaceHolder::class.java - ) - Logger.d( - TAG, - "View finder size: $width x $height" - ) - Logger.d(TAG, "Selected preview size: $previewSize") - surfaceView.setAspectRatio(previewSize.width, previewSize.height) - - // To ensure that size is set, initialize camera in the view's thread - post { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.CAMERA - ) != PackageManager.PERMISSION_GRANTED - ) { - throw SecurityException("Camera permission is needed to run this application") - } - it.startPreview(surfaceView, camera) - listener?.onPreviewStarted() - } - } catch (e: Exception) { - if (shouldFailSilently) { - Logger.w(TAG, e.toString(), e) - } else { - throw e - } - } + /** + * Stops the preview. + */ + private fun stopPreview() { + post { + stopPreviewInternal() + } + } + + private fun stopPreviewInternal() { + streamer?.stopPreview() + surfaceRequest?.markSurfaceSafeToRelease() + surfaceRequest = null + isPreviewing = false + } + + private fun startPreviewIfReady(targetViewSize: Size, shouldFailSilently: Boolean) { + try { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + throw SecurityException("Camera permission is needed to run this application") + } + + post { + startPreviewInternal(targetViewSize) + } + } catch (e: Exception) { + if (shouldFailSilently) { + Logger.w(TAG, e.toString(), e) + } else { + throw e } } } + private fun startPreviewInternal( + targetViewSize: Size + ) { + val streamer = streamer ?: run { + Logger.w(TAG, "Streamer has not been set") + return + } + if (width == 0 || height == 0) { + Logger.w(TAG, "View size is not ready") + return + } + if (isPreviewing) { + Logger.w(TAG, "Preview is already running") + return + } + isPreviewing = true + + Logger.d(TAG, "Target view size: $targetViewSize") + + val camera = defaultCameraId ?: streamer.camera + Logger.i(TAG, "Starting on camera: $camera") + + val request = createRequest(targetViewSize, camera) + surfaceRequest = request + + sendRequest(request, { surface -> + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + surfaceRequest?.markSurfaceSafeToRelease() + surfaceRequest = null + isPreviewing = false + Logger.e( + TAG, + "Camera permission is needed to run this application" + ) + } else { + streamer.startPreview(surface, camera) + listener?.onPreviewStarted() + } + }, { t -> + surfaceRequest?.markSurfaceSafeToRelease() + surfaceRequest = null + isPreviewing = false + Logger.w(TAG, "Failed to get a Surface: $t", t) + }) + } + + private fun createRequest( + targetViewSize: Size, + camera: String, + ): ViewfinderSurfaceRequest { + /** + * Get the closest available preview size to the view size. + */ + val previewSize = getPreviewOutputSize( + context.getCameraCharacteristics(camera), + targetViewSize, + SurfaceHolder::class.java + ) + + Logger.d(TAG, "Selected preview size: $previewSize") + + val builder = ViewfinderSurfaceRequest.Builder(previewSize) + builder.populateFromCharacteristics(context.getCameraCharacteristics(camera)) + + return builder.build() + } + + private fun sendRequest( + request: ViewfinderSurfaceRequest, + onSuccess: (Surface) -> Unit, + onFailure: (Throwable) -> Unit + ) { + val surfaceListenableFuture = + cameraViewFinder.requestSurfaceAsync(request) + + Futures.addCallback( + surfaceListenableFuture, + object : FutureCallback { + override fun onSuccess(surface: Surface) { + onSuccess(surface) + } + + override fun onFailure(t: Throwable) { + onFailure(t) + } + }, + ContextCompat.getMainExecutor(context) + ) + } + companion object { private const val TAG = "PreviewView" - - private val DEFAULT_CAMERA_FACING = FacingDirection.BACK - } - private inner class StreamerHolderCallback : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - startPreviewIfReady() + private val DEFAULT_CAMERA_FACING = CameraFacingDirection.BACK + + + private fun getPosition(scaleType: ScaleType): Position { + return when (scaleType) { + ScaleType.FILL_START -> Position.START + ScaleType.FILL_CENTER -> Position.CENTER + ScaleType.FILL_END -> Position.END + ScaleType.FIT_START -> Position.START + ScaleType.FIT_CENTER -> Position.CENTER + ScaleType.FIT_END -> Position.END + } + } + + private fun getScaleMode(scaleType: ScaleType): ScaleMode { + return when (scaleType) { + ScaleType.FILL_START -> ScaleMode.FILL + ScaleType.FILL_CENTER -> ScaleMode.FILL + ScaleType.FILL_END -> ScaleMode.FILL + ScaleType.FIT_START -> ScaleMode.FIT + ScaleType.FIT_CENTER -> ScaleMode.FIT + ScaleType.FIT_END -> ScaleMode.FIT + } } - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) = - Unit + private fun getScaleType(scaleMode: ScaleMode, position: Position): ScaleType { + when (position) { + Position.START -> { + return when (scaleMode) { + ScaleMode.FILL -> ScaleType.FILL_START + ScaleMode.FIT -> ScaleType.FIT_START + } + } + + Position.CENTER -> { + return when (scaleMode) { + ScaleMode.FILL -> ScaleType.FILL_CENTER + ScaleMode.FIT -> ScaleType.FIT_CENTER + } + } - override fun surfaceDestroyed(holder: SurfaceHolder) { - streamer?.stopPreview() + Position.END -> { + return when (scaleMode) { + ScaleMode.FILL -> ScaleType.FILL_END + ScaleMode.FIT -> ScaleType.FIT_END + } + } + } } } @@ -231,16 +432,85 @@ class PreviewView @JvmOverloads constructor( fun onPreviewStarted() {} fun onZoomRationOnPinchChanged(zoomRatio: Float) {} } -} -enum class FacingDirection(val value: String) { - FRONT("front"), - BACK("back"); + /** + * Options for the camera facing direction. + */ + enum class CameraFacingDirection(val value: Int) { + /** + * The facing of the camera is the same as that of the screen. + */ + FRONT(0), - companion object { - fun fromValue(value: String): FacingDirection { - return entries.first { it.value == value } + /** + * The facing of the camera is opposite to that of the screen. + */ + BACK(1); + + companion object { + /** + * Returns the [CameraFacingDirection] from the given id. + */ + internal fun entryOf(value: Int) = entries.first { it.value == value } } } -} + /** + * Options for the position of the [PreviewView] within its container. + */ + enum class Position(val value: Int) { + /** + * The [PreviewView] is positioned at the top of its container. + */ + START(0), + + /** + * The [PreviewView] is positioned in the center of its container. + */ + CENTER(1), + + /** + * The [PreviewView] is positioned in the bottom of its container. + */ + END(2); + + companion object { + /** + * Returns the [Position] from the given id. + */ + internal fun entryOf(value: Int) = entries.first { it.value == value } + } + } + + /** + * Options for scaling the [PreviewView] within its container. + */ + enum class ScaleMode(val value: Int) { + /** + * Scale the [PreviewView], maintaining the source aspect ratio, so it fills the entire + * parent. + * + * This may cause the [PreviewView] to be cropped. + */ + FILL(0), + + /** + * Scale the [PreviewView], maintaining the source aspect ratio, so it is entirely contained + * within the parent. The background area not covered by the viewfinder stream will be black + * or the background of the [PreviewView]. + * + * + * Both dimensions of the [PreviewView] will be equal or less than the corresponding + * dimensions of its parent. + */ + FIT(1); + + companion object { + /** + * Returns the [ScaleMode] from the given id. + */ + internal fun entryOf(value: Int) = entries.first { it.value == value } + } + } + +} diff --git a/core/src/main/java/io/github/thibaultbee/streampack/views/ViewExtensions.kt b/core/src/main/java/io/github/thibaultbee/streampack/views/ViewExtensions.kt new file mode 100644 index 000000000..45c32a79c --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/views/ViewExtensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Thibault B. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 io.github.thibaultbee.streampack.views + +import android.util.Size +import android.view.View + +/** + * Gets view size + */ +internal val View.size: Size + get() = Size(width, height) diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index d7fea8b71..480053030 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -1,8 +1,29 @@ + + + + + + + + + + + + + + + + + + + - + + + \ No newline at end of file diff --git a/demos/camera/src/main/res/layout/main_fragment.xml b/demos/camera/src/main/res/layout/main_fragment.xml index 12467fabd..9ecbf91eb 100644 --- a/demos/camera/src/main/res/layout/main_fragment.xml +++ b/demos/camera/src/main/res/layout/main_fragment.xml @@ -21,6 +21,8 @@ android:layout_height="match_parent" app:cameraFacingDirection="back" app:enableZoomOnPinch="true" + app:scaleMode="fill" + app:position="center" app:layout_constraintBottom_toTopOf="@+id/liveButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index f5bc94434..e1cbc061a 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -18,5 +18,7 @@ dependencies { implementation "androidx.core:core-ktx:${androidxCoreVersion}" testImplementation 'junit:junit:4.13.2' + + androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' } \ No newline at end of file diff --git a/extensions/srt/build.gradle b/extensions/srt/build.gradle index 0d84a9c23..410163df0 100644 --- a/extensions/srt/build.gradle +++ b/extensions/srt/build.gradle @@ -18,5 +18,7 @@ dependencies { implementation "androidx.core:core-ktx:${androidxCoreVersion}" testImplementation 'junit:junit:4.13.2' + + androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' } \ No newline at end of file