如何在ExoPlayer的PlayerView上具有类似的中心裁剪机制,但不在中心呢?

问题描述 投票:19回答:1

背景

我们记录用户面部的视频,通常面部位于视频的上半部分。

稍后我们希望观看视频,但是PlayerView的长宽比可能与视频之一不同,因此需要进行一些缩放和裁剪。

问题

我发现缩放PlayerView的唯一方法是将其显示在它具有的整个空间中,但要保持宽高比(当然,这会在需要时进行裁剪)是通过使用[C0 ]。这是一个与中心裁剪一起工作的示例:app:resize_mode="zoom"。显示内容的视图中具有相似长宽比的视图越多,所需的裁剪就越少。

但这仅用于中心,这意味着它需要视频的0.5x0.5的点,并从该点开始缩放。这导致丢失视频重要内容的许多情况。

例如,如果我们有一个以人像拍摄的视频,并且我们有一个方形的PlayerView并想显示顶部区域,那么这部分将是可见的:

http://s000.tinyupload.com/?file_id=00574047057406286563

当然,如果内容本身是正方形的,而视图也是正方形的,则它应该显示整个内容,而不会裁剪。

我尝试过的

我曾尝试通过Internet,StackOverflow(在此)和PlayerView进行搜索,但是我找不到执行该操作的方法。我发现的唯一线索是关于AspectRatioFrameLayout和AspectRatioTextureView,但即使有可能,我也没有找到如何将它们用于此任务的方法。

被告知(Github),我应该使用普通的here,并使用TextureView将其直接提供给SimpleExoPlayer。并使用SimpleExoPlayer.setVideoTextureView对其进行特殊转换。

[尝试了很多最好的用法之后(并查看了TextureView.setTransformvideo-crop repositorySuperImageView repository,其中有ImageView和视频的缩放比例的示例),我发布了一个工作示例,该示例似乎来正确显示视频,但我仍然不确定,因为我还使用了在开始播放之前在其顶部显示的ImageView(以实现更好的过渡而不是黑色内容)。

这是当前代码:

JCropImageView repository

在尝试进行此操作之前,我遇到了各种各样的问题,直到我了解到目前的情况,我已多次对此问题进行了多次更新。现在,它甚至可以与我所说的percentY一起使用,因此我可以根据需要将其设置为视频顶部的20%。但是,我仍然认为出问题的可能性很大,因为当我尝试将其设置为50%时,我注意到该内容可能不适合整个View。

我什至查看了ImageView(class MainActivity : AppCompatActivity() { private val imageResId = R.drawable.test private val videoResId = R.raw.test private val percentageY = 0.2f private var player: SimpleExoPlayer? = null override fun onCreate(savedInstanceState: Bundle?) { window.setBackgroundDrawable(ColorDrawable(0xff000000.toInt())) super.onCreate(savedInstanceState) if (cache == null) { cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES)) } setContentView(R.layout.activity_main) // imageView.visibility = View.INVISIBLE imageView.setImageResource(imageResId) imageView.doOnPreDraw { imageView.imageMatrix = prepareMatrixForImageView(imageView, imageView.drawable.intrinsicWidth.toFloat(), imageView.drawable.intrinsicHeight.toFloat()) // imageView.imageMatrix = prepareMatrix(imageView, imageView.drawable.intrinsicWidth.toFloat(), imageView.drawable.intrinsicHeight.toFloat()) // imageView.visibility = View.VISIBLE } } override fun onStart() { super.onStart() playVideo() } private fun prepareMatrix(view: View, contentWidth: Float, contentHeight: Float): Matrix { var scaleX = 1.0f var scaleY = 1.0f val viewWidth = view.measuredWidth.toFloat() val viewHeight = view.measuredHeight.toFloat() Log.d("AppLog", "viewWidth $viewWidth viewHeight $viewHeight contentWidth:$contentWidth contentHeight:$contentHeight") if (contentWidth > viewWidth && contentHeight > viewHeight) { scaleX = contentWidth / viewWidth scaleY = contentHeight / viewHeight } else if (contentWidth < viewWidth && contentHeight < viewHeight) { scaleY = viewWidth / contentWidth scaleX = viewHeight / contentHeight } else if (viewWidth > contentWidth) scaleY = viewWidth / contentWidth / (viewHeight / contentHeight) else if (viewHeight > contentHeight) scaleX = viewHeight / contentHeight / (viewWidth / contentWidth) val matrix = Matrix() val pivotPercentageX = 0.5f val pivotPercentageY = percentageY matrix.setScale(scaleX, scaleY, viewWidth * pivotPercentageX, viewHeight * pivotPercentageY) return matrix } private fun prepareMatrixForVideo(view: View, contentWidth: Float, contentHeight: Float): Matrix { val msWidth = view.measuredWidth val msHeight = view.measuredHeight val matrix = Matrix() matrix.setScale(1f, (contentHeight / contentWidth) * (msWidth.toFloat() / msHeight), msWidth / 2f, percentageY * msHeight) /*,msWidth/2f,msHeight/2f*/ return matrix } private fun prepareMatrixForImageView(view: View, contentWidth: Float, contentHeight: Float): Matrix { val dw = contentWidth val dh = contentHeight val msWidth = view.measuredWidth val msHeight = view.measuredHeight // Log.d("AppLog", "viewWidth $msWidth viewHeight $msHeight contentWidth:$contentWidth contentHeight:$contentHeight") val scalew = msWidth.toFloat() / dw val theoryh = (dh * scalew).toInt() val scaleh = msHeight.toFloat() / dh val theoryw = (dw * scaleh).toInt() val scale: Float var dx = 0 var dy = 0 if (scalew > scaleh) { // fit width scale = scalew // dy = ((msHeight - theoryh) * 0.0f + 0.5f).toInt() // + 0.5f for rounding } else { scale = scaleh dx = ((msWidth - theoryw) * 0.5f + 0.5f).toInt() // + 0.5f for rounding } dy = ((msHeight - theoryh) * percentageY + 0.5f).toInt() // + 0.5f for rounding val matrix = Matrix() // Log.d("AppLog", "scale:$scale dx:$dx dy:$dy") matrix.setScale(scale, scale) matrix.postTranslate(dx.toFloat(), dy.toFloat()) return matrix } private fun playVideo() { player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector()) player!!.setVideoTextureView(textureView) player!!.addVideoListener(object : VideoListener { override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) { super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio) Log.d("AppLog", "onVideoSizeChanged: $width $height") val videoWidth = if (unappliedRotationDegrees % 180 == 0) width else height val videoHeight = if (unappliedRotationDegrees % 180 == 0) height else width val matrix = prepareMatrixForVideo(textureView, videoWidth.toFloat(), videoHeight.toFloat()) textureView.setTransform(matrix) } override fun onRenderedFirstFrame() { Log.d("AppLog", "onRenderedFirstFrame") player!!.removeVideoListener(this) // imageView.animate().alpha(0f).setDuration(5000).start() imageView.visibility = View.INVISIBLE } }) player!!.volume = 0f player!!.repeatMode = Player.REPEAT_MODE_ALL player!!.playRawVideo(this, videoResId) player!!.playWhenReady = true // player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/240/big_buck_bunny_240p_20mb.mkv", cache!!) // player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv", cache!!) // player!!.playVideoFromUrl(this@MainActivity, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv") } override fun onStop() { super.onStop() player!!.setVideoTextureView(null) // playerView.player = null player!!.release() player = null } companion object { const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L var cache: com.google.android.exoplayer2.upstream.cache.Cache? = null @JvmStatic fun getUserAgent(context: Context): String { val packageManager = context.packageManager val info = packageManager.getPackageInfo(context.packageName, 0) val appName = info.applicationInfo.loadLabel(packageManager).toString() return Util.getUserAgent(context, appName) } } fun SimpleExoPlayer.playRawVideo(context: Context, @RawRes rawVideoRes: Int) { val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(rawVideoRes)) val rawResourceDataSource = RawResourceDataSource(context) rawResourceDataSource.open(dataSpec) val factory: DataSource.Factory = DataSource.Factory { rawResourceDataSource } prepare(LoopingMediaSource(ExtractorMediaSource.Factory(factory).createMediaSource(rawResourceDataSource.uri))) } fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String, cache: Cache? = null) = playVideoFromUri(context, Uri.parse(url), cache) fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file)) fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri, cache: Cache? = null) { val factory = if (cache != null) CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory(getUserAgent(context))) else DefaultDataSourceFactory(context, MainActivity.getUserAgent(context)) val mediaSource = ExtractorMediaSource.Factory(factory).createMediaSource(uri) prepare(mediaSource) } } )的源代码,以了解如何使用中心裁剪。当应用于ImageView时,它仍然可以用作中心裁剪,但是当我在视频上使用相同的技术时,它给我带来了非常错误的结果。

问题

我的目标是同时显示ImageView和视频,以便它可以从静态图像平稳过渡到视频。两者都具有从顶部到顶部的20%裁剪比例(例如)。我已经发布了一个示例项目here进行尝试,并与我分享发现的内容。

所以现在我的问题是,为什么这对于imageView和/或视频似乎无法正常工作:

  1. 事实证明,我尝试过的所有矩阵创建都不能很好地用于ImageView或视频。到底是怎么了?我怎样才能改变它们的外观呢?要从顶部20%缩放比例,例如?

  2. 我试图为两者都使用精确的矩阵,但是似乎两者都需要不同的格式,即使两者的大小和内容大小都相同。为什么我每个人都需要一个不同的矩阵?


编辑:回答了这个问题后,我决定制作一个小样的用法(Github存储库可用here):

here

如果需要,这里是单独的ImageView解决方案:

import android.content.Context
import android.graphics.Matrix
import android.graphics.PointF
import android.net.Uri
import android.os.Bundle
import android.view.TextureView
import android.view.View
import androidx.annotation.RawRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnPreDraw
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.LoopingMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.*
import com.google.android.exoplayer2.upstream.cache.Cache
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.google.android.exoplayer2.util.Util
import com.google.android.exoplayer2.video.VideoListener
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

// https://stackoverflow.com/questions/54216273/how-to-have-similar-mechanism-of-center-crop-on-exoplayers-playerview-but-not
class MainActivity : AppCompatActivity() {
    companion object {
        private val FOCAL_POINT = PointF(0.5f, 0.2f)
        private const val IMAGE_RES_ID = R.drawable.test
        private const val VIDEO_RES_ID = R.raw.test
        private var cache: Cache? = null
        private const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L

        @JvmStatic
        fun getUserAgent(context: Context): String {
            val packageManager = context.packageManager
            val info = packageManager.getPackageInfo(context.packageName, 0)
            val appName = info.applicationInfo.loadLabel(packageManager).toString()
            return Util.getUserAgent(context, appName)
        }
    }

    private var player: SimpleExoPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (cache == null)
            cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES))
        //        imageView.visibility = View.INVISIBLE
        imageView.setImageResource(IMAGE_RES_ID)
    }

    private fun prepareMatrix(view: View, mediaWidth: Float, mediaHeight: Float, focalPoint: PointF): Matrix? {
        if (view.visibility == View.GONE)
            return null
        val viewHeight = (view.height - view.paddingTop - view.paddingBottom).toFloat()
        val viewWidth = (view.width - view.paddingStart - view.paddingEnd).toFloat()
        if (viewWidth <= 0 || viewHeight <= 0)
            return null
        val matrix = Matrix()
        if (view is TextureView)
        // Restore true media size for further manipulation.
            matrix.setScale(mediaWidth / viewWidth, mediaHeight / viewHeight)
        val scaleFactorY = viewHeight / mediaHeight
        val scaleFactor: Float
        var px = 0f
        var py = 0f
        if (mediaWidth * scaleFactorY >= viewWidth) {
            // Fit height
            scaleFactor = scaleFactorY
            px = -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor)
        } else {
            // Fit width
            scaleFactor = viewWidth / mediaWidth
            py = -(mediaHeight * scaleFactor - viewHeight) * focalPoint.y / (1 - scaleFactor)
        }
        matrix.postScale(scaleFactor, scaleFactor, px, py)
        return matrix
    }

    private fun playVideo() {
        player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector())
        player!!.setVideoTextureView(textureView)
        player!!.addVideoListener(object : VideoListener {
            override fun onVideoSizeChanged(videoWidth: Int, videoHeight: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
                super.onVideoSizeChanged(videoWidth, videoHeight, unappliedRotationDegrees, pixelWidthHeightRatio)
                textureView.setTransform(prepareMatrix(textureView, videoWidth.toFloat(), videoHeight.toFloat(), FOCAL_POINT))
            }

            override fun onRenderedFirstFrame() {
                //                Log.d("AppLog", "onRenderedFirstFrame")
                player!!.removeVideoListener(this)
                imageView.animate().alpha(0f).setDuration(2000).start()
                //                imageView.visibility = View.INVISIBLE
            }
        })
        player!!.volume = 0f
        player!!.repeatMode = Player.REPEAT_MODE_ALL
        player!!.playRawVideo(this, VIDEO_RES_ID)
        player!!.playWhenReady = true
        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/240/big_buck_bunny_240p_20mb.mkv", cache!!)
        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv", cache!!)
        //        player!!.playVideoFromUrl(this@MainActivity, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")
    }

    override fun onStart() {
        super.onStart()
        imageView.doOnPreDraw {
            val imageWidth: Float = imageView.drawable.intrinsicWidth.toFloat()
            val imageHeight: Float = imageView.drawable.intrinsicHeight.toFloat()
            imageView.imageMatrix = prepareMatrix(imageView, imageWidth, imageHeight, FOCAL_POINT)
        }
        playVideo()
    }

    override fun onStop() {
        super.onStop()
        if (player != null) {
            player!!.setVideoTextureView(null)
            //        playerView.player = null
            player!!.release()
            player = null
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (!isChangingConfigurations)
            cache?.release()
    }

    fun SimpleExoPlayer.playRawVideo(context: Context, @RawRes rawVideoRes: Int) {
        val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(rawVideoRes))
        val rawResourceDataSource = RawResourceDataSource(context)
        rawResourceDataSource.open(dataSpec)
        val factory: DataSource.Factory = DataSource.Factory { rawResourceDataSource }
        prepare(LoopingMediaSource(ExtractorMediaSource.Factory(factory).createMediaSource(rawResourceDataSource.uri)))
    }

    fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String, cache: Cache? = null) = playVideoFromUri(context, Uri.parse(url), cache)

    fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))

    fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri, cache: Cache? = null) {
        val factory = if (cache != null)
            CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory(getUserAgent(context)))
        else
            DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))
        val mediaSource = ExtractorMediaSource.Factory(factory).createMediaSource(uri)
        prepare(mediaSource)
    }
}
android scale crop exoplayer
1个回答
9
投票

问题是如何像class ScaleCropImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) { var focalPoint = PointF(0.5f, 0.5f) set(value) { field = value updateMatrix() } private val viewWidth: Float get() = (width - paddingLeft - paddingRight).toFloat() private val viewHeight: Float get() = (height - paddingTop - paddingBottom).toFloat() init { scaleType = ScaleType.MATRIX } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) updateMatrix() } override fun setImageDrawable(drawable: Drawable?) { super.setImageDrawable(drawable) updateMatrix() } @Suppress("MemberVisibilityCanBePrivate") fun updateMatrix() { if (scaleType != ImageView.ScaleType.MATRIX) return val dr = drawable ?: return imageMatrix = prepareMatrix( viewWidth, viewHeight, dr.intrinsicWidth.toFloat(), dr.intrinsicHeight.toFloat(), focalPoint, Matrix() ) } private fun prepareMatrix( viewWidth: Float, viewHeight: Float, mediaWidth: Float, mediaHeight: Float, focalPoint: PointF, matrix: Matrix ): Matrix? { if (viewWidth <= 0 || viewHeight <= 0) return null var scaleFactor = viewHeight / mediaHeight if (mediaWidth * scaleFactor >= viewWidth) { // Fit height matrix.postScale(scaleFactor, scaleFactor, -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor), 0f) } else { // Fit width scaleFactor = viewWidth / mediaWidth matrix.postScale(scaleFactor, scaleFactor, 0f, -(mediaHeight * scaleFactor - viewHeight) * focalPoint.y / (1 - scaleFactor)) } return matrix } } 那样处理图像,但如何将焦点从中心移到距图像顶部20%的其他位置。首先,让我们看一下ImageView.ScaleType.CENTER_CROP的作用:

ImageView.ScaleType.CENTER_CROP

CENTER_CROP

均匀地缩放图像(保持图像的纵横比),以使图像的两个尺寸(宽度和高度)都等于或大于视图的相应尺寸(减去填充)。然后,图像在视图中居中。在XML中,使用以下语法:CENTER_CROP

换句话说,按比例缩放图像而不会变形,以使图像的宽度或高度(或宽度和高度)都适合视图,以使视图完全充满图像(没有间隙。)

另一种思考方式是将图像的中心“固定”在视图的中心。然后按比例缩放图像以满足上述条件。

在下面的视频中,白线标记图像的中心;红线标记视图的中心。标尺类型为documentation。注意图像和视图的中心点如何重合。当视图改变大小时,这两个点将继续重叠,并且始终显示在视图的中心,与视图大小无关。

android:scaleType="centerCrop"

所以,在不同位置(例如距顶部20%)具有中心作物样行为是什么意思?像中心裁切一样,我们可以指定将“固定”距图像顶部20%的点和“固定”距视图顶部20%的点,就像将50%点固定在中心裁切中一样。该点的水平位置保持在图像和视图的50%。现在可以缩放图像以满足中心裁剪的其他条件,这些条件指定图像的宽度和/或高度将适合视图且无间隙。 (视图大小应理解为视图大小减去填充。)>] >>

以下是这20%作物行为的简短视频。在此视频中,白线表示图像的中间,红线表示视图中的固定点,蓝线表示水平红线的后面表示图像顶部的20%。 (演示项目在CENTER_CROP上。

enter image description here

这里是显示提供的完整图像和从静止图像过渡的视频的方框图。 。

GitHub

MainActivity.kt

enter image description here是用于确定如何缩放/裁剪图像的方法。该视频还有一些其他工作要做,因为当将视频分配给enter image description here时,它看起来像是适合prepareMatrix()的比例类型“ FIT_XY”。由于这种缩放,必须在调用视频的TextureView之前恢复媒体大小
TextureView
© www.soinside.com 2019 - 2024. All rights reserved.