mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
fix(player): distinguish AAudio access and decoder failures
This commit is contained in:
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user