Release v0.2.43

This commit is contained in:
ponzischeme89
2026-08-10 13:09:38 +12:00
parent 12b519762b
commit 63f0768507
6 changed files with 59 additions and 14 deletions
+4
View File
@@ -1,3 +1,7 @@
## 0.2.43 — 2026-08-10
- Fixed: App no longer crashes.
- Fixed: Manual surround-sound choices now reliably override the automatically detected audio output.
## 0.2.42 — 2026-08-10
- Added: Memby's own opening clip now plays while the app is starting up.
- Added: Surround sound passthrough for Dolby Digital, Dolby Digital Plus, Dolby Atmos, DTS, DTS-HD and TrueHD, with automatic detection and manual receiver overrides.
+1 -1
View File
@@ -42,7 +42,7 @@ val projectNoticeText =
// A release workflow can derive the app version from its Git tag without editing the
// source tree. Local builds keep using the checked-in default.
val defaultVersionName = "0.2.42"
val defaultVersionName = "0.2.43"
val membyVersionName: String =
(project.findProperty("memby.versionName") as String?)
?.trim()
@@ -56,10 +56,13 @@ fun installAudioCapabilityProbe(context: Context) {
val deviceAudioCapabilities: DeviceAudioCapabilities
get() = AudioProbeHolder.cached ?: probeAudioCapabilities()
@OptIn(UnstableApi::class)
@UnstableApi
private fun probeAudioCapabilities(): DeviceAudioCapabilities {
val context = AudioProbeHolder.appContext ?: return DeviceAudioCapabilities()
watchAudioRoute(context)
// Route callbacks are useful but optional. A handful of vendor AudioManager
// implementations throw while registering one; that must not escape the otherwise
// defensive probe and take the launcher down with it.
runCatching { watchAudioRoute(context) }
val probed = runCatching { AndroidAudioCapabilityProbe(context).probe() }
// A vendor build with broken audio metadata must still play something: an empty
// probe means PCM through whatever the platform decoders can manage, which is the
@@ -131,7 +134,7 @@ private class AndroidAudioCapabilityProbe(private val context: Context) {
* built from, so the set the probe is asked about and the set the sink is told about can
* never fall out of step.
*/
@OptIn(UnstableApi::class)
@UnstableApi
internal fun SurroundCodec.encoding(): Int = when (this) {
SurroundCodec.AC3 -> C.ENCODING_AC3
SurroundCodec.EAC3 -> C.ENCODING_E_AC3
@@ -53,15 +53,33 @@ internal class MembyRenderersFactory(
context: Context,
enableFloatOutput: Boolean,
enableAudioTrackPlaybackParams: Boolean,
): AudioSink = DefaultAudioSink.Builder(context)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.apply {
manualAudioCapabilities(preference, capabilities)?.let(::setAudioCapabilities)
): AudioSink {
val manualCapabilities = manualAudioCapabilities(preference, capabilities)
val builder = if (manualCapabilities == null) {
DefaultAudioSink.Builder(context)
} else {
fixedCapabilitiesAudioSinkBuilder(manualCapabilities)
}
.build()
return builder
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.build()
}
}
/**
* Builds the fixed-capabilities sink used by manual mode.
*
* Media3 1.5 ignores [DefaultAudioSink.Builder.setAudioCapabilities] when the builder has
* a Context, because the live route receiver replaces the supplied value. Its deprecated
* context-free builder is therefore the only 1.5 API that can honour a viewer override.
*/
@Suppress("DEPRECATION")
private fun fixedCapabilitiesAudioSinkBuilder(
capabilities: AudioCapabilities,
): DefaultAudioSink.Builder = DefaultAudioSink.Builder()
.setAudioCapabilities(capabilities)
/**
* The fixed capabilities a manual choice becomes, or null in automatic mode — where the
* sink is left to track the hardware itself.
@@ -70,7 +88,8 @@ internal class MembyRenderersFactory(
* every decoded track is written as, and a capabilities object omitting it describes a
* television that cannot play audio at all.
*/
@OptIn(UnstableApi::class)
@UnstableApi
@Suppress("DEPRECATION")
internal fun manualAudioCapabilities(
preference: AudioPassthroughPreference,
capabilities: DeviceAudioCapabilities,
@@ -41,6 +41,21 @@ internal object PlayerEngine {
fun create(
context: Context,
audio: AudioPassthroughPreference = AudioPassthroughPreference.AUTOMATIC,
): ExoPlayer = try {
buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
} catch (_: RuntimeException) {
// Extension construction happens while ExoPlayer is built. A binary mismatch or
// broken vendor audio implementation must degrade to Android's renderers rather
// than make Memby close during its process-wide pre-roll warm-up.
buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
} catch (_: LinkageError) {
buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
}
private fun buildPlayer(
context: Context,
audio: AudioPassthroughPreference,
extensionRendererMode: Int,
): ExoPlayer = ExoPlayer.Builder(context)
.setMediaSourceFactory(mediaSourceFactory(context))
.setRenderersFactory(
@@ -52,7 +67,7 @@ internal object PlayerEngine {
// The bundled FFmpeg audio renderer sits after platform decoders. It is
// reached only when Android cannot decode a surround format itself; its
// output is PCM, so passthrough-capable tracks still bypass it untouched.
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON),
.setExtensionRendererMode(extensionRendererMode),
)
.setTrackSelector(DefaultTrackSelector(context))
.setLoadControl(loadControl())
@@ -40,9 +40,13 @@ internal object PrerollPreloader {
if (context != null) {
val cached = cachedPlayer
if (cached == null) {
cachedPlayer = createPreparedPlayer(context)
// This runs directly from the main looper rather than from a
// coroutine, so an exception here is otherwise an uncaught process
// crash. The opening clip is optional: a broken vendor audio stack or
// unavailable extension must skip it and leave the launcher running.
cachedPlayer = runCatching { createPreparedPlayer(context) }.getOrNull()
} else if (cached.playbackState == ExoPlayer.STATE_IDLE) {
cached.prepare()
runCatching { cached.prepare() }
}
}
false