Release v0.2.43
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,14 +53,32 @@ internal class MembyRenderersFactory(
|
||||
context: Context,
|
||||
enableFloatOutput: Boolean,
|
||||
enableAudioTrackPlaybackParams: Boolean,
|
||||
): AudioSink = DefaultAudioSink.Builder(context)
|
||||
): AudioSink {
|
||||
val manualCapabilities = manualAudioCapabilities(preference, capabilities)
|
||||
val builder = if (manualCapabilities == null) {
|
||||
DefaultAudioSink.Builder(context)
|
||||
} else {
|
||||
fixedCapabilitiesAudioSinkBuilder(manualCapabilities)
|
||||
}
|
||||
return builder
|
||||
.setEnableFloatOutput(enableFloatOutput)
|
||||
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
|
||||
.apply {
|
||||
manualAudioCapabilities(preference, capabilities)?.let(::setAudioCapabilities)
|
||||
}
|
||||
.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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user