【发布时间】:2019-06-10 11:50:00
【问题描述】:
背景
我们录制用户脸部的视频,通常脸部位于视频的上半部分。
稍后我们希望观看视频,但PlayerView 的宽高比可能与视频不同,因此需要进行一些缩放和裁剪。
问题
我发现缩放PlayerView 以便它显示在它拥有的整个空间中但保持纵横比(当然,这将导致在需要时进行裁剪)的唯一方法是使用@ 987654336@。这是它如何与 center-crop 一起使用的示例:http://s000.tinyupload.com/?file_id=00574047057406286563。显示内容的视图具有相似的纵横比越多,需要的裁剪就越少。
但这仅适用于中心,这意味着它需要视频的 0.5x0.5 点,并从该点进行缩放。这会导致很多情况下丢失视频的重要内容。
例如,如果我们有一个纵向拍摄的视频,并且我们有一个正方形的 PlayerView 并且想要显示顶部区域,那么这就是可见的部分:
当然,如果内容本身是方形的,视图也是方形的,应该显示整个内容,不要裁剪。
我尝试过的
我尝试在 Internet、StackOverflow(此处)和 Github 上进行搜索,但找不到方法。我发现的唯一线索是关于 AspectRatioFrameLayout 和 AspectRatioTextureView,但我没有找到如何将它们用于此任务,如果可能的话。
有人告诉我 (here) 我应该使用普通的 TextureView ,并使用 SimpleExoPlayer.setVideoTextureView 将其直接提供给 SimpleExoPlayer。并使用TextureView.setTransform 对其进行特殊转换。
经过大量尝试最好使用的方法(并查看 video-crop repository 、 SuperImageView repository 和 JCropImageView repository ,其中有 ImageView 和视频的缩放/裁剪示例),我发布了一个工作示例似乎可以正确显示视频,但我仍然不确定,因为我还使用了一个在它开始播放之前显示在其顶部的 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)
}
}
在了解当前情况之前,我在尝试此操作时遇到了各种问题,因此我已多次更新此问题。现在它甚至适用于我谈到的百分比,所以如果我愿意,我可以将其设置为视频顶部的 20%。但是,我仍然认为它很有可能出现问题,因为当我尝试将其设置为 50% 时,我注意到内容可能不适合整个 View。
我什至查看了 ImageView (here) 的源代码,了解如何使用 center-crop。当应用于 ImageView 时,它仍然可以作为 center-crop,但是当我在视频上使用相同的技术时,它给了我一个非常错误的结果。
问题
我的目标是同时显示 ImageView 和视频,以便从静态图像平滑过渡到视频。所有这一切,虽然两者都具有从顶部起 20% 的顶级作物(例如)。我已经发布了一个示例项目here 来试用它并与人们分享我的发现。
所以现在我的问题是为什么这似乎不适用于 imageView 和/或视频:
事实证明,我尝试过的所有矩阵创建都不适用于 ImageView 或视频。它到底有什么问题?我怎样才能改变它让它们看起来一样?例如,要从前 20% 进行规模种植?
我尝试对两者都使用精确矩阵,但似乎每个都需要不同的矩阵,即使两者具有完全相同的大小和内容大小。为什么我需要一个不同的矩阵?
编辑:在回答了这个问题后,我决定制作一个如何使用它的小示例(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
// 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)
}
}
如果需要,这里有一个单独的 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
}
}
【问题讨论】:
-
@MartinZeitler 是的,我知道。我检查了错误的变量。我的意思是检查度数,出于某种原因检查了宽度和高度......不过,我对 ImageView 和视频容器有疑问。请,如果您知道为什么会发生这种情况,请告诉我。
-
仅根据发布的图像,我怀疑这个主题是相关的:math.stackexchange.com/questions/180804/… ...其中视频纵横比和显示(或表面)纵横比都需要被考虑...为了得到一个看起来自然的结果。这些的某些组合可能需要大量裁剪 - 或有边界。还应该有一个计算最佳中心作物的公式。基本上它只是两个相互关联的矩形。
-
@MartinZeitler 但我在 ImageView 上使用了与视频完全相同的技术。两者都获得相同的视图宽度和高度以及函数内部的内容。
-
@MartinZeitler 我认为视频矩阵也不正确,而不仅仅是 ImageView。我希望我能尽快找到解决方案。
标签: android scale crop exoplayer