diff --git a/app/src/main/assets/codecs.json b/app/src/main/assets/codecs.json index 5336190..322fb0e 100644 --- a/app/src/main/assets/codecs.json +++ b/app/src/main/assets/codecs.json @@ -1,5 +1,5 @@ { - "_comment": "Optional overrides for codec priority (lower total score = higher priority). Tier bases: HW=0, SW=500, passthrough/raw=1000.", + "_comment": "Optional overrides for codec priority (lower total score = higher priority). Tier bases: HW=0, SW=500, passthrough/raw=1000. Default audio order: Opus < AAC < Speex < PCM.", "video": { "h264": { "priority_override_score": 0 diff --git a/app/src/main/java/com/foxx/androidcast/CastSettings.java b/app/src/main/java/com/foxx/androidcast/CastSettings.java index a9cb1eb..3020af2 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettings.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettings.java @@ -154,6 +154,8 @@ public class CastSettings implements Serializable { private ReceiverDisplayResolution receiverDisplayResolution = ReceiverDisplayResolution.AUTO_FIT; /** Filled after codec handshake (e.g. video/avc). */ private transient String negotiatedVideoMime; + /** Filled when AUTO (or fallback) resolves against peer audio caps. */ + private transient AudioCodec resolvedAudioCodec; public CastSettings() { PlatformCastSupport.applyDefaultAudioSetting(this); @@ -268,6 +270,14 @@ public class CastSettings implements Serializable { this.negotiatedVideoMime = negotiatedVideoMime; } + public AudioCodec getResolvedAudioCodec() { + return resolvedAudioCodec; + } + + public void setResolvedAudioCodec(AudioCodec resolvedAudioCodec) { + this.resolvedAudioCodec = resolvedAudioCodec; + } + public CaptureMode getCaptureMode() { return captureMode; } @@ -318,5 +328,6 @@ public class CastSettings implements Serializable { streamProtection = other.streamProtection; receiverDisplayResolution = other.receiverDisplayResolution; negotiatedVideoMime = other.negotiatedVideoMime; + resolvedAudioCodec = other.resolvedAudioCodec; } } diff --git a/app/src/main/java/com/foxx/androidcast/media/AudioNegotiator.java b/app/src/main/java/com/foxx/androidcast/media/AudioNegotiator.java new file mode 100644 index 0000000..9171956 --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/media/AudioNegotiator.java @@ -0,0 +1,85 @@ +package com.foxx.androidcast.media; + +import android.util.Log; + +import com.foxx.androidcast.CastSettings; +import com.foxx.androidcast.media.codec.PassthroughCodecPolicy; +import com.foxx.androidcast.network.CastCodecFlags; + +import java.util.Locale; + +/** + * Picks an audio codec both sides support, honoring user preference when possible. + * AUTO uses {@link CodecPriorityCatalog#decideBestAudioCodec} (weights + codecs.json). + */ +public final class AudioNegotiator { + private static final String TAG = "AudioNegotiator"; + + private AudioNegotiator() {} + + /** Resolve preference for this session (explicit or AUTO). */ + public static CastSettings.AudioCodec negotiate(int senderMask, int receiverMask, + CastSettings.AudioCodec preference, CastSettings settings) { + return negotiate(senderMask, receiverMask, preference, settings, "negotiate"); + } + + public static CastSettings.AudioCodec negotiate(int senderMask, int receiverMask, + CastSettings.AudioCodec preference, CastSettings settings, String logRole) { + CastSettings scoreSettings = settings != null ? settings : new CastSettings(); + CastSettings.AudioCodec pref = preference != null ? preference : CastSettings.AudioCodec.AUTO; + + if (pref == CastSettings.AudioCodec.AUTO) { + CodecPriorityCatalog.AudioNegotiationDecision decision = + CodecPriorityCatalog.decideBestAudioCodec(senderMask, receiverMask, scoreSettings); + if (decision != null) { + CodecPriorityCatalog.logAudioNegotiationDecision(logRole, decision); + return decision.codec; + } + Log.w(TAG, logRole + " audio AUTO fallback AAC (no scored candidate)"); + return CastSettings.AudioCodec.AAC; + } + + CastSettings.AudioCodec effective = PassthroughCodecPolicy.forAudioNegotiation(pref); + if (effective == CastSettings.AudioCodec.AUTO) { + return negotiate(senderMask, receiverMask, CastSettings.AudioCodec.AUTO, scoreSettings, logRole); + } + if (!CastCodecFlags.maskSupports(senderMask, effective) + || !CastCodecFlags.maskSupports(receiverMask, effective)) { + Log.w(TAG, logRole + " audio explicit " + pref.name() + + " unavailable on link; falling back to AUTO"); + return negotiate(senderMask, receiverMask, CastSettings.AudioCodec.AUTO, scoreSettings, logRole); + } + int score = CodecPriorityCatalog.priorityScore(effective, scoreSettings); + Log.i(TAG, String.format(Locale.US, "%s audio explicit %s score=%d", + logRole, PassthroughCodecPolicy.displayAudioPreference(effective), score)); + return effective; + } + + public static CastSettings.AudioCodec resolveForSender(CastSettings settings, int peerAudioMask) { + if (settings == null) { + return CastSettings.AudioCodec.AAC; + } + CastSettings.AudioCodec resolved = negotiate( + CastCodecFlags.localAudioMask(), + peerAudioMask, + settings.getAudioCodec(), + settings, + "sender"); + settings.setResolvedAudioCodec(resolved); + return resolved; + } + + public static CastSettings.AudioCodec effective(CastSettings settings) { + if (settings == null) { + return CastSettings.AudioCodec.AAC; + } + CastSettings.AudioCodec resolved = settings.getResolvedAudioCodec(); + if (resolved != null) { + return resolved; + } + if (settings.getAudioCodec() == CastSettings.AudioCodec.AUTO) { + return CastSettings.AudioCodec.AAC; + } + return PassthroughCodecPolicy.forAudioNegotiation(settings.getAudioCodec()); + } +} diff --git a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java index ce36985..b0bda29 100644 --- a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java @@ -17,6 +17,7 @@ import android.util.Log; import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; +import com.foxx.androidcast.network.CastCodecFlags; import com.foxx.androidcast.sender.CodecCatalog; import org.json.JSONObject; @@ -248,10 +249,10 @@ public final class CodecPriorityCatalog { public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) { if (preference == CastSettings.AudioCodec.SPEEX) { - return NativeCodecBridge.isSpeexAvailable() ? CodecTier.SOFTWARE : CodecTier.PASSTHROUGH; + return NativeCodecBridge.isSpeexAvailable() ? CodecTier.HARDWARE : CodecTier.PASSTHROUGH; } if (preference == CastSettings.AudioCodec.OPUS) { - return NativeCodecBridge.isOpusAvailable() ? CodecTier.SOFTWARE : CodecTier.PASSTHROUGH; + return NativeCodecBridge.isOpusAvailable() ? CodecTier.HARDWARE : CodecTier.PASSTHROUGH; } if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) { return CodecTier.PASSTHROUGH; @@ -259,6 +260,103 @@ public final class CodecPriorityCatalog { return CodecTier.HARDWARE; } + /** AUTO: lowest score among audio codecs both peers support on this device. */ + @Nullable + public static AudioNegotiationDecision decideBestAudioCodec(int senderMask, int receiverMask, + CastSettings settings) { + int common = senderMask & receiverMask; + List candidates = new ArrayList<>(); + for (CastSettings.AudioCodec codec : manualAudioPreferences()) { + if (!CastCodecFlags.maskSupports(common, codec)) { + continue; + } + if (!CastCodecFlags.isLocallyAvailable(codec)) { + continue; + } + int score = audioCodecScoreOnDevice(codec, settings); + candidates.add(new AudioNegotiationCandidate(codec, score, + formatAudioBreakdown(codec, settings))); + } + if (candidates.isEmpty()) { + return null; + } + Collections.sort(candidates, Comparator.comparingInt(c -> c.score)); + return new AudioNegotiationDecision(candidates.get(0), candidates); + } + + public static int audioCodecScoreOnDevice(CastSettings.AudioCodec codec, CastSettings settings) { + CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec); + if (id == null) { + return Integer.MAX_VALUE; + } + CodecTier tier = tierForAudioPreference(codec); + return priorityScore(id, tier, settings); + } + + public static void logAudioNegotiationDecision(String role, AudioNegotiationDecision decision) { + if (decision == null) { + return; + } + Log.i(TAG, role + " audio AUTO: selected " + decision.logLine()); + for (AudioNegotiationCandidate c : decision.candidates) { + boolean picked = c.codec == decision.codec; + Log.i(TAG, role + " audio candidate: " + c.logLine(picked)); + } + } + + public static String formatAudioBreakdown(CastSettings.AudioCodec codec, CastSettings settings) { + CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec); + if (id == null) { + return codec.name(); + } + ScoreBreakdown b = breakdown(id, tierForAudioPreference(codec), settings); + return String.format(Locale.US, "%s score=%d tier=%d +%d+%d+%d+%d+%d", + codec.name(), b.total, b.tierBase, b.internal, b.fec, b.protection, b.override, b.bitrate); + } + + /** Manual settings order (after AUTO): Opus, AAC, Speex, PCM passthrough. */ + public static CastSettings.AudioCodec[] manualAudioPreferences() { + return new CastSettings.AudioCodec[] { + CastSettings.AudioCodec.OPUS, + CastSettings.AudioCodec.AAC, + CastSettings.AudioCodec.SPEEX, + CastSettings.AudioCodec.PASSTHROUGH_DEBUG, + }; + } + + public static final class AudioNegotiationCandidate { + public final CastSettings.AudioCodec codec; + public final int score; + public final String detail; + + AudioNegotiationCandidate(CastSettings.AudioCodec codec, int score, String detail) { + this.codec = codec; + this.score = score; + this.detail = detail; + } + + String logLine(boolean selected) { + return (selected ? "* " : " ") + codec.name() + " score=" + score + " · " + detail; + } + } + + public static final class AudioNegotiationDecision { + public final CastSettings.AudioCodec codec; + public final int score; + public final List candidates; + + AudioNegotiationDecision(AudioNegotiationCandidate selected, + List candidates) { + this.codec = selected.codec; + this.score = selected.score; + this.candidates = candidates; + } + + String logLine() { + return codec.name() + " score=" + score; + } + } + public static List sortByPriority(List> items) { List> copy = new ArrayList<>(items); Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore)); diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java index 6cb9e8b..e72c0a1 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java @@ -12,12 +12,19 @@ package com.foxx.androidcast.media.codec; * Digest: SHA256 e8b2c92d5f3a97150adcf9af3f44791d2ee6a0db31dd7b7d96cb970bedc5e9fd **********************************************************************/ import com.foxx.androidcast.CastSettings; +import com.foxx.androidcast.media.AudioNegotiator; import com.foxx.androidcast.sender.AudioEncoder; /** Creates audio encoder sinks. */ public final class AudioCodecFactory { private AudioCodecFactory() {} + public static AudioEncoderSink createEncoder(CastSettings settings) { + CastSettings.AudioCodec preference = settings != null + ? AudioNegotiator.effective(settings) : CastSettings.AudioCodec.AAC; + return createEncoder(preference); + } + public static AudioEncoderSink createEncoder(CastSettings.AudioCodec preference) { CodecSessionRegistry.setAudioPreference(preference); AudioCodecBackend backend = PassthroughCodecPolicy.audioBackendFor(preference, true); diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java index aec8396..aa1c317 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java @@ -164,7 +164,7 @@ public final class PassthroughCodecPolicy { return "Passthrough (debug)"; case AUTO: default: - return "AUTO (AAC)"; + return "AUTO"; } } } diff --git a/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java b/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java index 5a81f0c..c4e47f1 100644 --- a/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java +++ b/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java @@ -13,6 +13,7 @@ package com.foxx.androidcast.network; **********************************************************************/ import android.media.MediaFormat; +import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.sender.CodecCatalog; @@ -71,4 +72,51 @@ public final class CastCodecFlags { } return mask; } + + public static int localDecoderAudioMask() { + return localAudioMask(); + } + + public static int flagForAudioPreference(CastSettings.AudioCodec codec) { + if (codec == null) { + return AUDIO_UNKNOWN; + } + switch (codec) { + case OPUS: + return AUDIO_OPUS; + case SPEEX: + return AUDIO_SPEEX; + case PASSTHROUGH_DEBUG: + return AUDIO_PCM; + case AAC: + return AUDIO_AAC; + case AUTO: + default: + return AUDIO_UNKNOWN; + } + } + + public static boolean maskSupports(int mask, CastSettings.AudioCodec codec) { + int flag = flagForAudioPreference(codec); + return flag != AUDIO_UNKNOWN && (mask & flag) != 0; + } + + /** Whether this device can encode/decode the preference (native probes for Opus/Speex). */ + public static boolean isLocallyAvailable(CastSettings.AudioCodec codec) { + if (codec == null) { + return false; + } + switch (codec) { + case OPUS: + return NativeCodecBridge.isOpusAvailable(); + case SPEEX: + return NativeCodecBridge.isSpeexAvailable(); + case AAC: + case PASSTHROUGH_DEBUG: + return true; + case AUTO: + default: + return false; + } + } } diff --git a/app/src/main/java/com/foxx/androidcast/receiver/ReceiverCastService.java b/app/src/main/java/com/foxx/androidcast/receiver/ReceiverCastService.java index 134c80f..d042b5e 100644 --- a/app/src/main/java/com/foxx/androidcast/receiver/ReceiverCastService.java +++ b/app/src/main/java/com/foxx/androidcast/receiver/ReceiverCastService.java @@ -744,16 +744,18 @@ public class ReceiverCastService extends Service { : android.media.MediaFormat.MIMETYPE_VIDEO_AVC; CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime); if (remoteCastSettings != null) { + CastSettings.AudioCodec effectiveAudio = + com.foxx.androidcast.media.AudioNegotiator.effective(remoteCastSettings); CodecSessionRegistry.setVideoPreference(remoteCastSettings.getVideoCodec()); - CodecSessionRegistry.setAudioPreference(remoteCastSettings.getAudioCodec()); + CodecSessionRegistry.setAudioPreference(effectiveAudio); CodecSessionRegistry.setAudioBackend( PassthroughCodecPolicy.audioBackendFor( - remoteCastSettings.getAudioCodec(), + effectiveAudio, remoteCastSettings.isAudioRequested())); CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription( remoteCastSettings.getStreamProtection(), remoteCastSettings.getTransport(), - remoteCastSettings.getAudioCodec())); + effectiveAudio)); } videoDecoder.configure(mime, pendingWidth, pendingHeight, pendingCsd0, pendingCsd1, attachedSurface, new VideoDecoderSink.Listener() { diff --git a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java index e1634ae..663955c 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java @@ -91,11 +91,10 @@ public final class CodecCatalog { boolean opus = NativeCodecBridge.isOpusAvailable(); boolean speex = NativeCodecBridge.isSpeexAvailable(); out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0)); - out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings)); out.add(scoredAudio(CastSettings.AudioCodec.OPUS, opus, settings)); + out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings)); out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, speex, settings)); out.add(scoredAudio(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, true, settings)); - sortAudioChoices(out); return out; } diff --git a/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java b/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java index 1a62f33..6c0ae0f 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java +++ b/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java @@ -16,6 +16,7 @@ import android.util.Log; import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.DeviceInfo; +import com.foxx.androidcast.media.AudioNegotiator; import com.foxx.androidcast.network.CastCodecFlags; import com.foxx.androidcast.network.CastFramingContext; import com.foxx.androidcast.network.CastProtocol; @@ -113,8 +114,10 @@ public final class MultiCastCoordinator { connectWithRetry(session, t.host, t.port); CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings); applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement); + AudioNegotiator.resolveForSender(peerSettings, t.audioCodecs); peers.add(new ActivePeer(t, session, hs.settings, hs.negotiatedVideoMime)); Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime + + " audio=" + peerSettings.getResolvedAudioCodec() + " score=" + com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime( hs.negotiatedVideoMime, hs.settings)); @@ -142,6 +145,7 @@ public final class MultiCastCoordinator { connectWithRetry(session, target.host, target.port); CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings); applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement); + AudioNegotiator.resolveForSender(peerSettings, target.audioCodecs); peers.add(new ActivePeer(target, session, hs.settings, hs.negotiatedVideoMime)); if (keyframeRequester != null) { keyframeRequester.requestKeyframe(); diff --git a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java index fd8ac26..9160c3f 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java +++ b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java @@ -55,6 +55,7 @@ import com.foxx.androidcast.network.control.PassThroughNetworkControlPlane; import com.foxx.androidcast.network.transport.TransportLossStats; import com.foxx.androidcast.network.transport.TransportLossStatsBridge; import com.foxx.androidcast.network.selftest.NetworkSelfTestBridge; +import com.foxx.androidcast.media.AudioNegotiator; import com.foxx.androidcast.media.codec.AudioCodecFactory; import com.foxx.androidcast.media.codec.CodecSessionRegistry; import com.foxx.androidcast.media.codec.PassthroughCodecPolicy; @@ -274,6 +275,7 @@ public class ScreenCastService extends Service implements session = primary.session; settings = primary.settings; settings.setNegotiatedVideoMime(primary.negotiatedMime); + AudioNegotiator.resolveForSender(settings, primary.target.audioCodecs); final CastSettings captureSettings = settings; updateStatus(getString(R.string.authenticating)); networkFeedback = new NetworkFeedbackManager(session, true); @@ -303,7 +305,9 @@ public class ScreenCastService extends Service implements } sessionStatsRecorder.mergeSettings(settings); sessionStatsRecorder.setClientCount(peers.size()); - sessionStatsRecorder.setCodecs(settings.getNegotiatedVideoMime(), "AAC"); + sessionStatsRecorder.setCodecs(settings.getNegotiatedVideoMime(), + PassthroughCodecPolicy.displayAudioPreference( + AudioNegotiator.effective(settings))); } final int projResultCode = resultCode; final Intent projData = data != null ? new Intent(data) : null; @@ -806,9 +810,10 @@ public class ScreenCastService extends Service implements if (audioCapture != null) { audioCapture.stop(); } - audioEncoder = AudioCodecFactory.createEncoder(settings.getAudioCodec()); + CastSettings.AudioCodec effectiveAudio = AudioNegotiator.effective(settings); + audioEncoder = AudioCodecFactory.createEncoder(settings); CodecSessionRegistry.setAudioBackend( - PassthroughCodecPolicy.audioBackendFor(settings.getAudioCodec(), true)); + PassthroughCodecPolicy.audioBackendFor(effectiveAudio, true)); audioEncoder.prepare(new AudioEncoderSink.Callback() { private boolean configSent; diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bb6fa0b..b3d4266 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -233,7 +233,7 @@ Speex Opus Passthrough (отладка) - AUTO (AAC) + AUTO Картинка в картинке (PiP) Режим «картинка в картинке» (PiP) недоступен на этом устройстве PiP отключено diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fcfc70..8d37f0c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -325,7 +325,7 @@ Speex Opus Passthrough (debug) - AUTO (AAC) + AUTO Picture-in-picture Picture-in-picture is not available on this device Picture-in-picture disabled diff --git a/app/src/test/java/com/foxx/androidcast/media/AudioNegotiatorTest.java b/app/src/test/java/com/foxx/androidcast/media/AudioNegotiatorTest.java new file mode 100644 index 0000000..e830e90 --- /dev/null +++ b/app/src/test/java/com/foxx/androidcast/media/AudioNegotiatorTest.java @@ -0,0 +1,61 @@ +package com.foxx.androidcast.media; + +import com.foxx.androidcast.CastSettings; +import com.foxx.androidcast.network.CastCodecFlags; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class AudioNegotiatorTest { + + @Test + public void autoPrefersAacOverPcmPassthrough() { + CastSettings settings = new CastSettings(); + int mask = CastCodecFlags.AUDIO_AAC | CastCodecFlags.AUDIO_PCM; + + CodecPriorityCatalog.AudioNegotiationDecision decision = + CodecPriorityCatalog.decideBestAudioCodec(mask, mask, settings); + assertNotNull(decision); + assertEquals(CastSettings.AudioCodec.AAC, decision.codec); + } + + @Test + public void compileTimeScoreOrderAacBeforePcm() { + CastSettings settings = new CastSettings(); + int aac = CodecPriorityCatalog.priorityScore(CastSettings.AudioCodec.AAC, settings); + int pcm = CodecPriorityCatalog.priorityScore(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, settings); + assertTrue(aac < pcm); + } + + @Test + public void autoFallsBackToAacWithoutOpusOnLink() { + CastSettings settings = new CastSettings(); + int mask = CastCodecFlags.AUDIO_AAC | CastCodecFlags.AUDIO_PCM; + + CastSettings.AudioCodec picked = AudioNegotiator.negotiate( + mask, mask, CastSettings.AudioCodec.AUTO, settings); + assertEquals(CastSettings.AudioCodec.AAC, picked); + } + + @Test + public void explicitOpusUnavailableFallsBackToAuto() { + CastSettings settings = new CastSettings(); + int mask = CastCodecFlags.AUDIO_AAC; + + CastSettings.AudioCodec picked = AudioNegotiator.negotiate( + mask, mask, CastSettings.AudioCodec.OPUS, settings); + assertEquals(CastSettings.AudioCodec.AAC, picked); + } + + @Test + public void manualPreferenceOrderIsFixedInCatalog() { + CastSettings.AudioCodec[] order = CodecPriorityCatalog.manualAudioPreferences(); + assertEquals(CastSettings.AudioCodec.OPUS, order[0]); + assertEquals(CastSettings.AudioCodec.AAC, order[1]); + assertEquals(CastSettings.AudioCodec.SPEEX, order[2]); + assertEquals(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, order[3]); + } +}