如何在 ExoPlayer 的 PlayerView 上有类似的 center-crop 机制,而不是在中心?

How to have similar mechanism of center-crop on ExoPlayer's PlayerView , but not on the center?

背景

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

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

问题

我发现缩放 PlayerView 的唯一方法,这样它将在整个 space 中显示,但保持纵横比(这将在需要时裁剪,当然),是通过使用 app:resize_mode="zoom" 。这是它如何与 center-crop 一起使用的示例: http://s000.tinyupload.com/?file_id=00574047057406286563 。显示内容的 View 的宽高比越相似,需要的裁剪就越少。

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

例如,如果我们有一个纵向拍摄的视频,并且我们有一个方形的 PlayerView 并且想要显示顶部区域,这就是可见的部分:

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

我试过的

我已经尝试通过 Internet、Whosebug(此处)和 Github 进行搜索,但找不到如何进行搜索。我找到的唯一线索是关于 AspectRatioFrameLayout 和 AspectRatioTextureView,但我没有找到如何将它们用于此任务,如果可能的话。

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

经过大量尝试最适合使用的方法(并查看 video-crop repository , SuperImageView repository , and JCropImageView repository,其中包含 ImageView 和视频的 scale/crop 示例),我发布了一个工作示例,似乎显示视频正确,但我仍然不确定,因为我还使用了在开始播放之前显示在其顶部的 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)
    }
}

在遇到目前的情况之前,我在尝试这个问题时遇到了各种问题,因此我已经相应地多次更新了这个问题。现在它甚至适用于我所说的 percentageY,所以我可以将它设置为视频顶部的 20%,如果我愿意的话。但是,我仍然认为它很有可能出了问题,因为当我尝试将其设置为 50% 时,我注意到内容可能不适合整个视图。

我还看了ImageView的源码(here),看看center-crop是怎么用的。当应用于 ImageView 时,它仍然作为中心裁剪工作,但是当我在视频上使用相同的技术时,它给了我一个非常错误的结果。

问题

我的目标是同时显示 ImageView 和视频,以便它从静态图像平滑过渡到视频。所有这些同时都具有从顶部 20% 的顶级裁剪(例如)。我已经发布了一个示例项目 here 来试用它并与人们分享我的发现。

所以现在我的问题是为什么这对 imageView and/or 视频似乎效果不佳:

  1. 事实证明,我尝试过的 none 矩阵创作对 ImageView 或视频都适用。它到底有什么问题?我怎样才能改变它让他们看起来一样?例如,从前 20% 裁剪?

  2. 我尝试对两者使用完全相同的矩阵,但似乎每个人都需要不同的矩阵,即使两者具有完全相同的大小和内容大小。为什么每个都需要不同的矩阵?


编辑:在回答了这个问题后,我决定制作一个小样本来说明如何使用它(Github 存储库可用 here):

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

// 
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)
    }
}

如果需要,这里有一个单独针对 ImageView 的解决方案:

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
    }
}

问题是如何操纵像 ImageView.ScaleType.CENTER_CROP 这样的图像,但将焦点从中心转移到距离图像顶部 20% 的另一个位置。首先,让我们看看 CENTER_CROP 做了什么:

来自documentation

CENTER_CROP

Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding). The image is then centered in the view. From XML, use this syntax: android:scaleType="centerCrop".

换句话说,在不失真的情况下缩放图像,使图像的宽度或高度(或宽度和高度)适合视图,从而使视图完全充满图像(无间隙)。

另一种理解方式是图像的中心 "pinned" 到视图的中心。然后缩放图像以满足上述标准。

在下面的视频中,白线标记了图像的中心;红线标记视图的中心。刻度类型为 CENTER_CROP。注意图像和视图的中心点是如何重合的。随着视图改变大小,这两个点继续重叠并始终出现在视图的中心,无论视图大小如何。

那么,在不同的位置(例如距离顶部 20%)有类似中心裁剪的行为是什么意思?像中心裁剪一样,我们可以指定距离图像顶部 20% 的点和距离视图顶部 20% 的点将是 "pinned",就像 50% 的点是 "pinned"在中心作物。该点的水平位置保持在图像和视图的 50% 处。现在可以缩放图像以满足中心裁剪的其他条件,这些条件指定图像的宽度 and/or 高度将无间隙地适合视图。 (视图大小理解为视图大小减去填充。)

这里是这个 20% 裁剪行为的短视频。在此视频中,白线显示图像的中间,红线显示视图中的固定点,显示在水平红线后面的蓝线表示距离图像顶部 20%。 (演示项目在 GitHub.

这里的结果显示了提供的完整图像和方形框架中从静止图像过渡的视频。 .

MainActivity.kt
prepareMatrix() 是确定如何 scale/crop 图像的方法。视频还有一些额外的工作要做,因为当视频被分配给 TextureView 时,它似乎适合 TextureView 作为比例类型 "FIT_XY"。由于这种缩放,在为视频

调用 prepareMatrix() 之前必须恢复媒体大小
class MainActivity : AppCompatActivity() {
    private val imageResId = R.drawable.test
    private val videoResId = R.raw.test
    private var player: SimpleExoPlayer? = null
    private val mFocalPoint = PointF(0.5f, 0.2f)

    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.scaleType = ImageView.ScaleType.MATRIX
            val imageWidth: Float = ContextCompat.getDrawable(this, imageResId)!!.intrinsicWidth.toFloat()
            val imageHeight: Float = ContextCompat.getDrawable(this, imageResId)!!.intrinsicHeight.toFloat()
            imageView.imageMatrix = prepareMatrix(imageView, imageWidth, imageHeight, mFocalPoint, Matrix())
            val b = BitmapFactory.decodeResource(resources, imageResId)
            val d = BitmapDrawable(resources, b.copy(Bitmap.Config.ARGB_8888, true))
            val c = Canvas(d.bitmap)
            val p = Paint()
            p.color = resources.getColor(android.R.color.holo_red_dark)
            p.style = Paint.Style.STROKE
            val strokeWidth = 10
            p.strokeWidth = strokeWidth.toFloat()
            // Horizontal line
            c.drawLine(0f, imageHeight * mFocalPoint.y, imageWidth, imageHeight * mFocalPoint.y, p)
            // Vertical line
            c.drawLine(imageWidth * mFocalPoint.x, 0f, imageWidth * mFocalPoint.x, imageHeight, p)
            // Line in horizontal and vertical center
            p.color = resources.getColor(android.R.color.white)
            c.drawLine(imageWidth / 2, 0f, imageWidth / 2, imageHeight, p)
            c.drawLine(0f, imageHeight / 2, imageWidth, imageHeight / 2, p)

            imageView.setImageBitmap(d.bitmap)
            imageViewFull.setImageBitmap(d.bitmap)
        }
    }

    fun startPlay(view: View) {
        playVideo()
    }

    private fun getViewWidth(view: View): Float {
        return (view.width - view.paddingStart - view.paddingEnd).toFloat()
    }

    private fun getViewHeight(view: View): Float {
        return (view.height - view.paddingTop - view.paddingBottom).toFloat()
    }

    private fun prepareMatrix(targetView: View, mediaWidth: Float, mediaHeight: Float,
                              focalPoint: PointF, matrix: Matrix): Matrix {
        if (targetView.visibility != View.VISIBLE) {
            return matrix
        }
        val viewHeight = getViewHeight(targetView)
        val viewWidth = getViewWidth(targetView)
        val scaleFactorY = viewHeight / mediaHeight
        val scaleFactor: Float
        val px: Float
        val py: Float
        if (mediaWidth * scaleFactorY >= viewWidth) {
            // Fit height
            scaleFactor = scaleFactorY
            px = -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor)
            py = 0f
        } else {
            // Fit width
            scaleFactor = viewWidth / mediaWidth
            px = 0f
            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(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
                super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio)
                val matrix = Matrix()
                // Restore true media size for further manipulation.
                matrix.setScale(width / getViewWidth(textureView), height / getViewHeight(textureView))
                textureView.setTransform(prepareMatrix(textureView, width.toFloat(), height.toFloat(), mFocalPoint, matrix))
            }

            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, 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()
        if (player != null) {
            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)
    }
}

您可以在 com.google.android 中使用 app:resize_mode="缩放"。exoplayer2.ui.PlayerView

我有一个类似的问题,并通过对 ExoPlayer 使用的 TextureView 应用转换解决了这个问题:


player.addVideoListener(object : VideoListener {
    override fun onVideoSizeChanged(
        videoWidth: Int,
        videoHeight: Int,
        unappliedRotationDegrees: Int,
        pixelWidthHeightRatio: Float,
    ) {
        removeVideoListener(this)
        val viewWidth: Int = textureView.width - textureView.paddingStart - textureView.paddingEnd
        val viewHeight: Int = textureView.height - textureView.paddingTop - textureView.paddingBottom
        if (videoWidth == viewWidth && videoHeight == viewHeight) {
            return
        }
        val matrix = Matrix().apply {
            // TextureView makes a best effort in fitting the video inside the View. The first transformation we apply is for reverting the fitting.
            setScale(
                videoWidth.toFloat() / viewWidth,
                videoHeight.toFloat() / viewHeight,
            )
        }
        
        // This algorithm is from ImageView's CENTER_CROP transformation
        val offset = 0.5f // the center in CENTER_CROP but you probably want a different value here
        val scale: Float
        val dx: Float
        val dy: Float
        if (videoWidth * viewHeight > viewWidth * videoHeight) {
            scale = viewHeight.toFloat() / videoHeight
            dx = (viewWidth - videoWidth * scale) * offset
            dy = 0f
        } else {
            scale = viewWidth.toFloat() / videoWidth
            dx = 0f
            dy = (viewHeight - videoHeight * scale) * offset
        }
        setTransform(matrix.apply {
            postScale(scale, scale)
            postTranslate(dx, dy)
        })
    }
})
player.setVideoTextureView(textureView)
player.prepare(createMediaSource())

请注意,除非您使用的是 DefaultRenderersFactory,否则您需要确保您的视频 Renderer 实际调用了 onVideoSizeChanged,例如通过如下方式创建工厂:

val renderersFactory = RenderersFactory { handler, videoListener, _, _, _, _ ->
        // Allows other renderers to be removed by R8
        arrayOf(
            MediaCodecVideoRenderer(
                context,
                MediaCodecSelector.DEFAULT,
                DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,
                handler,
                videoListener,
                -1,
            ),
            MediaCodecAudioRenderer(context, MediaCodecSelector.DEFAULT),
        )
    }