fix(player): distinguish AAudio access and decoder failures

This commit is contained in:
zarzet committed 2026-09-27 15:26:11 +07:00
1 parent d92bcf7716
commit e0f5a96564
6 files changed
+55 -6

No files matched your search

@@ -96,12 +96,34 @@ class HiResPlaybackTest {
} finally { playback.dispose(); file.delete() }
}
@Test
fun invalidFileIsNotReportedAsExclusiveAccessFailure() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val file = File.createTempFile("invalid-pcm-", ".wav", context.cacheDir)
file.writeBytes(ByteArray(64))
val playback = HiResPlayback(context) {}
try {
val done = CountDownLatch(1)
var reply: Any? = null
var error: String? = null
playback.command(MethodCall("prepare", mapOf("path" to file.path, "token" to 1))) { value, failure ->
reply = value; error = failure; done.countDown()
}
assertTrue(done.await(10, TimeUnit.SECONDS))
assertNull(error)
assertEquals("format", (reply as Map<*, *>)["reason"])
assertEquals(false, (reply as Map<*, *>)["fatal"])
} finally { playback.dispose(); file.delete() }
}
@Test
fun exclusiveOutputEitherVerifiesExactFormatOrRejectsSharedFallback() {
assertThrows(IllegalStateException::class.java) { NativeAudio.openOboe(1, 2, 24, 0) }
val handle = try { NativeAudio.openOboe(48000, 2, 16, 0) }
catch (error: IllegalStateException) {
assertTrue(error.message.orEmpty().contains("exclusive output unavailable"))
assertTrue(error.message.orEmpty().contains("requested=48000Hz/2ch/16bit"))
assertTrue(error.message.orEmpty().contains("format="))
return
}
try {
+16 -2
View File
@@ -9,6 +9,7 @@
#include <memory>
#include <limits>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <vector>
@@ -72,6 +73,9 @@ public:
if (precision <= 24) formats.push_back(oboe::AudioFormat::I24);
formats.push_back(oboe::AudioFormat::I32);
if (precision <= 24) formats.push_back(oboe::AudioFormat::Float);
std::ostringstream attempts;
attempts << "requested=" << rate << "Hz/" << channels << "ch/" << precision
<< "bit, device=" << device;
for (auto format : formats) {
oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output)
@@ -84,7 +88,12 @@ public:
->setDeviceId(device)->setDataCallback(this)
->setChannelConversionAllowed(false)->setFormatConversionAllowed(false)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None);
if (builder.openStream(stream) != oboe::Result::OK) continue;
const auto result = builder.openStream(stream);
attempts << "; format=" << oboe::convertToText(format);
if (result != oboe::Result::OK) {
attempts << " open=" << oboe::convertToText(result);
continue;
}
// Exclusive is a request: Android can silently return Shared. Reject
// that stream instead of displaying a misleading exclusive badge.
if (stream->getAudioApi() == oboe::AudioApi::AAudio &&
@@ -97,10 +106,15 @@ public:
ring.resize(static_cast<size_t>(rate / 4) * frameBytes);
return;
}
attempts << " actual=" << oboe::convertToText(stream->getSharingMode())
<< "/" << stream->getSampleRate() << "Hz/" << stream->getChannelCount()
<< "ch/" << oboe::convertToText(stream->getFormat())
<< ", api=" << oboe::convertToText(stream->getAudioApi())
<< ", device=" << stream->getDeviceId();
stream->close();
stream.reset();
}
throw std::runtime_error("Exact-rate AAudio exclusive output unavailable");
throw std::runtime_error("Exact-rate AAudio exclusive output unavailable: " + attempts.str());
}
~ExclusiveOutput() override { if (stream) stream->close(); }
@@ -70,6 +70,7 @@ internal class HiResPlayback(context: Context, private val emit: (Map<String, An
if (Build.VERSION.SDK_INT < 27) return mapOf("reason" to "exclusive_unavailable")
token = call.argument<Int>("token") ?: 0
var isDsd = call.argument<Boolean>("requiresDsd") == true
var failureReason = "format"
try {
val path = requireNotNull(call.argument<String>("path"))
isDsd = isDsd || DsdSource.isDsd(path)
@@ -78,9 +79,11 @@ internal class HiResPlayback(context: Context, private val emit: (Map<String, An
val pcm = UsbPcmSource(path).also { source = it }
val devices = manager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
val wired = devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
failureReason = "exclusive_unavailable"
output = NativeAudio.openOboe(pcm.rate, pcm.channels, pcm.bits, wired?.id ?: 0)
bits = NativeAudio.bitsOboe(output)
device = NativeAudio.deviceOboe(output)
failureReason = "format"
val deadline = android.os.SystemClock.elapsedRealtime() + 5000
while (pending == null && !pcm.ended) {
require(expected == revision) { "AAudio prepare cancelled" }
@@ -93,9 +96,12 @@ internal class HiResPlayback(context: Context, private val emit: (Map<String, An
"sampleRate" to pcm.rate, "bitDepth" to pcm.bits, "duration" to pcm.durationUs / 1000,
"device" to (devices.firstOrNull { it.id == device }?.productName?.toString() ?: "AAudio"))
} catch (error: Exception) {
android.util.Log.i("HiResPlayback", "Exclusive output unavailable: ${error.message}")
close()
return mapOf("reason" to if (isDsd) "dsd_unsupported" else "exclusive_unavailable", "fatal" to isDsd)
if (expected != revision) return mapOf("reason" to "cancelled")
val reason = if (isDsd) "dsd_unsupported" else failureReason
val detail = error.message.orEmpty().take(3000)
android.util.Log.i("HiResPlayback", "$reason: $detail")
return mapOf("reason" to reason, "fatal" to isDsd, "detail" to detail)
}
}