feat(player): 增加进度条拖动画面预览
This commit is contained in:
1 parent
d06dce9753
commit
51c56d884b
15 files changed
+1493
-77
No files matched your search
@@ -5,8 +5,13 @@ package com.stormg.lumadrama
|
||||
import android.content.Intent
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Rational
|
||||
import android.content.pm.PackageManager
|
||||
import android.view.WindowManager
|
||||
@@ -15,12 +20,19 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val shareRequestCode = 9041
|
||||
private val thumbnailExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
private val thumbnailHandler = Handler(Looper.getMainLooper())
|
||||
private var pendingShare: MethodChannel.Result? = null
|
||||
private var pipChannel: MethodChannel? = null
|
||||
private var keyboardChannel: MethodChannel? = null
|
||||
private var thumbnailChannel: MethodChannel? = null
|
||||
private var imeInset = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -86,6 +98,47 @@ class MainActivity : FlutterActivity() {
|
||||
emitImeInset(inset)
|
||||
result.success(inset)
|
||||
}
|
||||
thumbnailChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"com.stormg.lumadrama/thumbnail"
|
||||
)
|
||||
thumbnailChannel?.setMethodCallHandler { call, result ->
|
||||
if (call.method != "frame") {
|
||||
result.notImplemented()
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val timeMs = (call.argument<Number>("timeMs"))?.toLong()
|
||||
if (timeMs == null) {
|
||||
result.success(null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val localPath = call.argument<String>("localPath").orEmpty()
|
||||
val remoteUrl = call.argument<String>("url").orEmpty()
|
||||
val rawHeaders = call.argument<Map<*, *>>("headers")
|
||||
val headers = rawHeaders?.entries
|
||||
?.associate { it.key.toString() to it.value.toString() }
|
||||
.orEmpty()
|
||||
val responded = AtomicBoolean(false)
|
||||
val task = thumbnailExecutor.submit {
|
||||
val bytes = renderThumbnail(
|
||||
timeMs = timeMs,
|
||||
localPath = localPath,
|
||||
remoteUrl = remoteUrl,
|
||||
headers = headers
|
||||
)
|
||||
thumbnailHandler.post {
|
||||
if (responded.compareAndSet(false, true)) {
|
||||
result.success(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnailHandler.postDelayed({
|
||||
if (responded.compareAndSet(false, true)) {
|
||||
task.cancel(true)
|
||||
result.success(null)
|
||||
}
|
||||
}, thumbnailTimeoutMs)
|
||||
}
|
||||
val decorView = window.decorView
|
||||
ViewCompat.setOnApplyWindowInsetsListener(decorView) { view, insets ->
|
||||
emitImeInset(insets.getInsets(WindowInsetsCompat.Type.ime()).bottom)
|
||||
@@ -106,6 +159,59 @@ class MainActivity : FlutterActivity() {
|
||||
keyboardChannel?.invokeMethod("insetsChanged", value)
|
||||
}
|
||||
|
||||
private fun renderThumbnail(
|
||||
timeMs: Long,
|
||||
localPath: String,
|
||||
remoteUrl: String,
|
||||
headers: Map<String, String>
|
||||
): ByteArray? {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
var frame: Bitmap? = null
|
||||
var scaled: Bitmap? = null
|
||||
return try {
|
||||
when {
|
||||
localPath.isNotEmpty() -> retriever.setDataSource(normalizeLocalPath(localPath))
|
||||
remoteUrl.isNotEmpty() -> retriever.setDataSource(remoteUrl, headers)
|
||||
else -> return null
|
||||
}
|
||||
frame = retriever.getFrameAtTime(
|
||||
timeMs * 1000,
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||
)
|
||||
val source = frame ?: return null
|
||||
scaled = if (source.width > thumbnailMaxWidth) {
|
||||
val targetHeight = (source.height * thumbnailMaxWidth / source.width)
|
||||
.coerceAtLeast(1)
|
||||
Bitmap.createScaledBitmap(
|
||||
source,
|
||||
thumbnailMaxWidth,
|
||||
targetHeight,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
source
|
||||
}
|
||||
val outputBitmap = scaled ?: source
|
||||
val output = ByteArrayOutputStream()
|
||||
val compressed = outputBitmap.compress(
|
||||
Bitmap.CompressFormat.JPEG,
|
||||
thumbnailJpegQuality,
|
||||
output
|
||||
)
|
||||
if (!compressed) return null
|
||||
output.toByteArray()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} finally {
|
||||
if (scaled != null && scaled !== frame) scaled.recycle()
|
||||
frame?.recycle()
|
||||
retriever.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeLocalPath(path: String): String =
|
||||
if (path.startsWith("file:")) Uri.parse(path).path ?: path else path
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
pipChannel?.invokeMethod("onPipChanged", isInPictureInPictureMode)
|
||||
@@ -124,6 +230,16 @@ class MainActivity : FlutterActivity() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView, null)
|
||||
keyboardChannel?.setMethodCallHandler(null)
|
||||
keyboardChannel = null
|
||||
thumbnailChannel?.setMethodCallHandler(null)
|
||||
thumbnailChannel = null
|
||||
thumbnailHandler.removeCallbacksAndMessages(null)
|
||||
thumbnailExecutor.shutdownNow()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val thumbnailMaxWidth = 240
|
||||
private const val thumbnailJpegQuality = 70
|
||||
private const val thumbnailTimeoutMs = 1500L
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user