mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Add experimental MPV player backend (#161)
Experimental MPV player backend Related to #14, #22, & #85 You can switch to MPV in advanced settings and toggle using hardware decoding or not. This uses code and buildscripts from https://github.com/mpv-android/mpv-android, plus other third party libraries to build MPV
This commit is contained in:
parent
d7b333e519
commit
6be2662d4e
84 changed files with 3987 additions and 289 deletions
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
|||
import com.github.damontecres.wholphin.util.Version
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ class AppUpgradeHandler
|
|||
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
|
||||
}
|
||||
try {
|
||||
copySubfont(true)
|
||||
upgradeApp(
|
||||
context,
|
||||
Version.Companion.fromString(previousVersion ?: "0.0.0"),
|
||||
|
|
@ -60,6 +62,27 @@ class AppUpgradeHandler
|
|||
}
|
||||
}
|
||||
|
||||
fun copySubfont(overwrite: Boolean) {
|
||||
try {
|
||||
val fontFileName = "subfont.ttf"
|
||||
val outputFile = File(context.filesDir, fontFileName)
|
||||
if (!outputFile.exists() || overwrite) {
|
||||
context.assets.open(fontFileName).use { input ->
|
||||
outputFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
Timber.i("Wrote font %s to local", fontFileName)
|
||||
}
|
||||
// val oldFontDir = File(context.filesDir, "fonts")
|
||||
// if (oldFontDir.exists()) {
|
||||
// oldFontDir.deleteRecursively()
|
||||
// }
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception copying subfont.tff")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VERSION_NAME_PREVIOUS_KEY = "version.previous.name"
|
||||
const val VERSION_CODE_PREVIOUS_KEY = "version.previous.code"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import androidx.media3.common.Player
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -38,27 +41,45 @@ class PlayerFactory
|
|||
currentPlayer?.release()
|
||||
}
|
||||
|
||||
val extensions =
|
||||
runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled
|
||||
Timber.v("extensions=$extensions")
|
||||
val rendererMode =
|
||||
when (extensions) {
|
||||
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
}
|
||||
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
|
||||
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
|
||||
val newPlayer =
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
.setRenderersFactory(
|
||||
DefaultRenderersFactory(context)
|
||||
.setEnableDecoderFallback(true)
|
||||
.setExtensionRendererMode(rendererMode),
|
||||
).build()
|
||||
.apply {
|
||||
playWhenReady = true
|
||||
when (backend) {
|
||||
PlayerBackend.MPV -> {
|
||||
val enableHardwareDecoding =
|
||||
prefs?.mpvOptions?.enableHardwareDecoding
|
||||
?: AppPreference.MpvHardwareDecoding.defaultValue
|
||||
MpvPlayer(context, enableHardwareDecoding)
|
||||
.apply {
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
PlayerBackend.EXO_PLAYER,
|
||||
PlayerBackend.UNRECOGNIZED,
|
||||
-> {
|
||||
val extensions =
|
||||
runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled
|
||||
Timber.v("extensions=$extensions")
|
||||
val rendererMode =
|
||||
when (extensions) {
|
||||
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
}
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
.setRenderersFactory(
|
||||
DefaultRenderersFactory(context)
|
||||
.setEnableDecoderFallback(true)
|
||||
.setExtensionRendererMode(rendererMode),
|
||||
).build()
|
||||
.apply {
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
currentPlayer = newPlayer
|
||||
return newPlayer
|
||||
}
|
||||
|
|
@ -68,6 +89,7 @@ val Player.isReleased: Boolean
|
|||
get() {
|
||||
return when (this) {
|
||||
is ExoPlayer -> isReleased
|
||||
is MpvPlayer -> isReleased
|
||||
else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
|
|
@ -51,9 +53,8 @@ class UpdateChecker
|
|||
) {
|
||||
companion object {
|
||||
// TODO apk names
|
||||
private const val ASSET_NAME = "Wholphin.apk"
|
||||
private const val DEBUG_ASSET_NAME = "Wholphin-debug.apk"
|
||||
private const val RELEASE_ASSET_NAME = "Wholphin-release.apk"
|
||||
private const val ASSET_NAME = "Wholphin"
|
||||
private const val APK_NAME = "$ASSET_NAME.apk"
|
||||
|
||||
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
||||
|
||||
|
|
@ -116,15 +117,10 @@ class UpdateChecker
|
|||
|
||||
suspend fun getLatestRelease(updateUrl: String): Release? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val preferredAsset =
|
||||
if (PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean("updatePreferRelease", true)
|
||||
) {
|
||||
RELEASE_ASSET_NAME
|
||||
} else {
|
||||
DEBUG_ASSET_NAME
|
||||
}
|
||||
val preferRelease =
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean("updatePreferRelease", true)
|
||||
|
||||
val request =
|
||||
Request
|
||||
|
|
@ -136,21 +132,15 @@ class UpdateChecker
|
|||
if (it.isSuccessful && it.body != null) {
|
||||
val result = Json.parseToJsonElement(it.body!!.string())
|
||||
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||
val version = Version.Companion.tryFromString(name)
|
||||
val version = Version.tryFromString(name)
|
||||
val publishedAt =
|
||||
result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull
|
||||
val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull
|
||||
val downloadUrl =
|
||||
result.jsonObject["assets"]
|
||||
?.jsonArray
|
||||
?.firstOrNull { asset ->
|
||||
val assetName =
|
||||
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||
assetName == ASSET_NAME || assetName == preferredAsset
|
||||
}?.jsonObject
|
||||
?.get("browser_download_url")
|
||||
?.jsonPrimitive
|
||||
?.contentOrNull
|
||||
?.let { assets -> getDownloadUrl(assets, preferRelease) }
|
||||
Timber.v("version=$version, downloadUrl=$downloadUrl")
|
||||
if (version != null) {
|
||||
val notes =
|
||||
if (body.isNotNullOrBlank()) {
|
||||
|
|
@ -174,6 +164,35 @@ class UpdateChecker
|
|||
}
|
||||
}
|
||||
|
||||
private fun getDownloadUrl(
|
||||
assets: JsonArray,
|
||||
preferRelease: Boolean,
|
||||
): String? {
|
||||
val abiSuffix = Build.SUPPORTED_ABIS.firstOrNull().let { if (it != null) "-$it" else "" }
|
||||
val releaseSuffix = if (preferRelease) "-release" else "-debug"
|
||||
val preferredNames =
|
||||
listOf(
|
||||
"$ASSET_NAME${releaseSuffix}$abiSuffix.apk",
|
||||
"$ASSET_NAME$releaseSuffix.apk",
|
||||
"$ASSET_NAME.apk",
|
||||
)
|
||||
var preferredAsset: JsonObject? = null
|
||||
outer@ for (name in preferredNames) {
|
||||
for (asset in assets) {
|
||||
val assetName =
|
||||
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||
if (name == assetName) {
|
||||
preferredAsset = asset.jsonObject
|
||||
break@outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return preferredAsset
|
||||
?.get("browser_download_url")
|
||||
?.jsonPrimitive
|
||||
?.contentOrNull
|
||||
}
|
||||
|
||||
suspend fun installRelease(
|
||||
release: Release,
|
||||
callback: DownloadCallback,
|
||||
|
|
@ -195,7 +214,7 @@ class UpdateChecker
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues =
|
||||
ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, ASSET_NAME)
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, APK_NAME)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, APK_MIME_TYPE)
|
||||
put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
|
|
@ -267,7 +286,7 @@ class UpdateChecker
|
|||
val downloadDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
downloadDir.mkdirs()
|
||||
val targetFile = File(downloadDir, ASSET_NAME)
|
||||
val targetFile = File(downloadDir, APK_NAME)
|
||||
targetFile.outputStream().use { output ->
|
||||
response.body!!.byteStream().use { input ->
|
||||
copyTo(input, output, callback = callback)
|
||||
|
|
@ -320,7 +339,7 @@ class UpdateChecker
|
|||
} else {
|
||||
val downloadDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val targetFile = File(downloadDir, ASSET_NAME)
|
||||
val targetFile = File(downloadDir, APK_NAME)
|
||||
if (targetFile.exists()) {
|
||||
targetFile.delete()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue