Use SurfaceView on Android

This commit is contained in:
Florian Märkl 2021-01-14 18:56:42 +01:00
commit 510064c899
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
6 changed files with 206 additions and 80 deletions

View file

@ -26,13 +26,11 @@ ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *dec
return chiaki_mutex_init(&decoder->codec_mutex, false); return chiaki_mutex_init(&decoder->codec_mutex, false);
} }
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder) static void kill_decoder(AndroidChiakiVideoDecoder *decoder)
{
if(decoder->codec)
{ {
chiaki_mutex_lock(&decoder->codec_mutex); chiaki_mutex_lock(&decoder->codec_mutex);
decoder->shutdown_output = true; decoder->shutdown_output = true;
ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1); ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000);
if(codec_buf_index >= 0) if(codec_buf_index >= 0)
{ {
CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer");
@ -48,7 +46,14 @@ void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
chiaki_mutex_unlock(&decoder->codec_mutex); chiaki_mutex_unlock(&decoder->codec_mutex);
} }
AMediaCodec_delete(decoder->codec); AMediaCodec_delete(decoder->codec);
decoder->codec = NULL;
decoder->shutdown_output = false;
} }
void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder)
{
if(decoder->codec)
kill_decoder(decoder);
chiaki_mutex_fini(&decoder->codec_mutex); chiaki_mutex_fini(&decoder->codec_mutex);
} }
@ -56,6 +61,16 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder
{ {
chiaki_mutex_lock(&decoder->codec_mutex); chiaki_mutex_lock(&decoder->codec_mutex);
if(!surface)
{
if(decoder->codec)
{
kill_decoder(decoder);
CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed");
}
return;
}
if(decoder->codec) if(decoder->codec)
{ {
#if __ANDROID_API__ >= 23 #if __ANDROID_API__ >= 23

View file

@ -91,7 +91,7 @@ private class ChiakiNative
@JvmStatic external fun sessionStart(ptr: Long): Int @JvmStatic external fun sessionStart(ptr: Long): Int
@JvmStatic external fun sessionStop(ptr: Long): Int @JvmStatic external fun sessionStop(ptr: Long): Int
@JvmStatic external fun sessionJoin(ptr: Long): Int @JvmStatic external fun sessionJoin(ptr: Long): Int
@JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface) @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?)
@JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState) @JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState)
@JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String) @JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String)
@JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService) @JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService)
@ -350,7 +350,7 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean)
event(RumbleEvent(left.toUByte(), right.toUByte())) event(RumbleEvent(left.toUByte(), right.toUByte()))
} }
fun setSurface(surface: Surface) fun setSurface(surface: Surface?)
{ {
ChiakiNative.sessionSetSurface(nativePtr, surface) ChiakiNative.sessionSetSurface(nativePtr, surface)
} }

View file

@ -28,7 +28,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
private val _rumbleState = MutableLiveData<RumbleEvent>(RumbleEvent(0U, 0U)) private val _rumbleState = MutableLiveData<RumbleEvent>(RumbleEvent(0U, 0U))
val rumbleState: LiveData<RumbleEvent> get() = _rumbleState val rumbleState: LiveData<RumbleEvent> get() = _rumbleState
var surfaceTexture: SurfaceTexture? = null private var surfaceTexture: SurfaceTexture? = null
private var surface: Surface? = null
init init
{ {
@ -61,9 +62,9 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
_state.value = StreamStateConnecting _state.value = StreamStateConnecting
session.eventCallback = this::eventCallback session.eventCallback = this::eventCallback
session.start() session.start()
val surfaceTexture = surfaceTexture val surface = surface
if(surfaceTexture != null) if(surface != null)
session.setSurface(Surface(surfaceTexture)) session.setSurface(surface)
this.session = session this.session = session
} }
catch(e: CreateError) catch(e: CreateError)
@ -92,6 +93,26 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
} }
} }
fun attachToSurfaceView(surfaceView: SurfaceView)
{
surfaceView.holder.addCallback(object: SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) { }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int)
{
val surface = holder.surface
this@StreamSession.surface = surface
session?.setSurface(surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder)
{
this@StreamSession.surface = null
session?.setSurface(null)
}
})
}
fun attachToTextureView(textureView: TextureView) fun attachToTextureView(textureView: TextureView)
{ {
textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener { textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener {
@ -100,6 +121,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va
if(surfaceTexture != null) if(surfaceTexture != null)
return return
surfaceTexture = surface surfaceTexture = surface
this@StreamSession.surface = Surface(surfaceTexture)
session?.setSurface(Surface(surface)) session?.setSurface(Surface(surface))
} }

View file

@ -0,0 +1,68 @@
package com.metallic.chiaki.stream
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
// see ExoPlayer's AspectRatioFrameLayout
class AspectRatioFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs)
{
companion object
{
private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f
}
var aspectRatio = 0f
set(value)
{
if(field != value)
{
field = value
requestLayout()
}
}
var mode: TransformMode = TransformMode.FIT
set(value)
{
if(field != value)
{
field = value
requestLayout()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if(aspectRatio <= 0)
{
// Aspect ratio not set.
return
}
var width = measuredWidth
var height = measuredHeight
val viewAspectRatio = width.toFloat() / height
val aspectDeformation = aspectRatio / viewAspectRatio - 1
if(Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION)
return
when(mode)
{
TransformMode.ZOOM ->
if(aspectDeformation > 0)
width = (height * aspectRatio).toInt()
else
height = (width / aspectRatio).toInt()
TransformMode.FIT ->
if(aspectDeformation > 0)
height = (width / aspectRatio).toInt()
else
width = (height * aspectRatio).toInt()
TransformMode.STRETCH -> {}
}
super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
}
}

View file

@ -7,12 +7,12 @@ import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog import android.app.AlertDialog
import android.graphics.Matrix import android.graphics.Matrix
import android.os.* import android.os.*
import android.view.KeyEvent import android.transition.TransitionManager
import android.view.MotionEvent import android.view.*
import android.view.TextureView
import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -44,6 +44,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
private lateinit var viewModel: StreamViewModel private lateinit var viewModel: StreamViewModel
private val uiVisibilityHandler = Handler() private val uiVisibilityHandler = Handler()
private val streamView: View get() = surfaceView
override fun onCreate(savedInstanceState: Bundle?) override fun onCreate(savedInstanceState: Bundle?)
{ {
@ -94,15 +95,17 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
if (displayModeToggle.checkedButtonId == -1) if (displayModeToggle.checkedButtonId == -1)
displayModeToggle.check(checkedId) displayModeToggle.check(checkedId)
adjustTextureViewAspect() adjustStreamViewAspect()
showOverlay() showOverlay()
} }
viewModel.session.attachToTextureView(textureView) //viewModel.session.attachToTextureView(textureView)
viewModel.session.attachToSurfaceView(surfaceView)
viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) viewModel.session.state.observe(this, Observer { this.stateChanged(it) })
textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> /*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
adjustTextureViewAspect() adjustStreamViewAspect()
} }*/
adjustStreamViewAspect()
if(Preferences(this).rumbleEnabled) if(Preferences(this).rumbleEnabled)
{ {
@ -306,73 +309,84 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe
} }
} }
private fun adjustTextureViewAspect() private fun adjustTextureViewAspect(textureView: TextureView)
{ {
val displayInfo = DisplayInfo(viewModel.session.connectInfo.videoProfile, textureView) val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView)
val resolution = displayInfo.computeResolutionFor(displayModeToggle.checkedButtonId) val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId))
Matrix().also { Matrix().also {
textureView.getTransform(it) textureView.getTransform(it)
it.setScale(resolution.width / displayInfo.viewWidth, resolution.height / displayInfo.viewHeight) it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight)
it.postTranslate((displayInfo.viewWidth - resolution.width) * 0.5f, (displayInfo.viewHeight - resolution.height) * 0.5f) it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f)
textureView.setTransform(it) textureView.setTransform(it)
} }
} }
private fun adjustSurfaceViewAspect()
{
val videoProfile = viewModel.session.connectInfo.videoProfile
aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat()
aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId)
}
private fun adjustStreamViewAspect() = adjustSurfaceViewAspect()
override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event)
} }
enum class TransformMode
class DisplayInfo constructor(val videoProfile: ConnectVideoProfile, val textureView: TextureView)
{ {
val contentWidth : Float get() = videoProfile.width.toFloat() FIT,
val contentHeight : Float get() = videoProfile.height.toFloat() STRETCH,
ZOOM;
companion object
{
fun fromButton(displayModeButtonId: Int)
= when (displayModeButtonId)
{
R.id.display_mode_stretch_button -> STRETCH
R.id.display_mode_zoom_button -> ZOOM
else -> FIT
}
}
}
class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView)
{
private val contentWidth : Float get() = videoProfile.width.toFloat()
private val contentHeight : Float get() = videoProfile.height.toFloat()
val viewWidth : Float get() = textureView.width.toFloat() val viewWidth : Float get() = textureView.width.toFloat()
val viewHeight : Float get() = textureView.height.toFloat() val viewHeight : Float get() = textureView.height.toFloat()
val contentAspect : Float get() = contentHeight / contentWidth private val contentAspect : Float get() = contentHeight / contentWidth
fun computeResolutionFor(displayModeButtonId: Int) : Resolution fun resolutionFor(mode: TransformMode): Resolution
= when(mode)
{ {
when (displayModeButtonId) TransformMode.STRETCH -> strechedResolution
{ TransformMode.ZOOM -> zoomedResolution
R.id.display_mode_stretch_button -> return computeStrechedResolution() TransformMode.FIT -> normalResolution
R.id.display_mode_zoom_button -> return computeZoomedResolution()
else -> return computeNormalResolution()
}
} }
private fun computeStrechedResolution(): Resolution private val strechedResolution get() = Resolution(viewWidth, viewHeight)
{
return Resolution(viewWidth, viewHeight)
}
private fun computeZoomedResolution(): Resolution private val zoomedResolution get() =
{
if(viewHeight > viewWidth * contentAspect) if(viewHeight > viewWidth * contentAspect)
{ {
val zoomFactor = viewHeight / contentHeight val zoomFactor = viewHeight / contentHeight
return Resolution(contentWidth * zoomFactor, viewHeight) Resolution(contentWidth * zoomFactor, viewHeight)
} }
else else
{ {
val zoomFactor = viewWidth / contentWidth val zoomFactor = viewWidth / contentWidth
return Resolution(viewWidth, contentHeight * zoomFactor) Resolution(viewWidth, contentHeight * zoomFactor)
}
} }
private fun computeNormalResolution(): Resolution private val normalResolution get() =
{
if(viewHeight > viewWidth * contentAspect) if(viewHeight > viewWidth * contentAspect)
{ Resolution(viewWidth, viewWidth * contentAspect)
return Resolution(viewWidth, viewWidth * contentAspect)
}
else else
{ Resolution(viewHeight / contentAspect, viewHeight)
return Resolution(viewHeight / contentAspect, viewHeight)
}
}
} }

View file

@ -1,16 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mainStreamLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".stream.StreamActivity" tools:context=".stream.StreamActivity"
android:keepScreenOn="true"> android:keepScreenOn="true">
<TextureView <com.metallic.chiaki.stream.AspectRatioFrameLayout
android:id="@+id/textureView" android:id="@+id/aspectRatioLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
android:layout_gravity="center">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.metallic.chiaki.stream.AspectRatioFrameLayout>
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
@ -34,7 +42,6 @@
android:id="@+id/overlay" android:id="@+id/overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="bottom"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout