1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:39:09 +03:00

Audio codec AUTO negotiation and fixed manual priority order.

Add AudioNegotiator with weighted AUTO selection (codecs.json + peer caps),
fixed settings order (Opus, AAC, Speex, PCM), and resolve at handshake.
Native Opus/Speex encode/decode remains future work; stubs still wire AAC.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-06-02 22:07:32 +02:00
parent 0738b53959
commit 4e045f68e6
14 changed files with 334 additions and 14 deletions

View File

@@ -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": { "video": {
"h264": { "h264": {
"priority_override_score": 0 "priority_override_score": 0

View File

@@ -154,6 +154,8 @@ public class CastSettings implements Serializable {
private ReceiverDisplayResolution receiverDisplayResolution = ReceiverDisplayResolution.AUTO_FIT; private ReceiverDisplayResolution receiverDisplayResolution = ReceiverDisplayResolution.AUTO_FIT;
/** Filled after codec handshake (e.g. video/avc). */ /** Filled after codec handshake (e.g. video/avc). */
private transient String negotiatedVideoMime; private transient String negotiatedVideoMime;
/** Filled when AUTO (or fallback) resolves against peer audio caps. */
private transient AudioCodec resolvedAudioCodec;
public CastSettings() { public CastSettings() {
PlatformCastSupport.applyDefaultAudioSetting(this); PlatformCastSupport.applyDefaultAudioSetting(this);
@@ -268,6 +270,14 @@ public class CastSettings implements Serializable {
this.negotiatedVideoMime = negotiatedVideoMime; this.negotiatedVideoMime = negotiatedVideoMime;
} }
public AudioCodec getResolvedAudioCodec() {
return resolvedAudioCodec;
}
public void setResolvedAudioCodec(AudioCodec resolvedAudioCodec) {
this.resolvedAudioCodec = resolvedAudioCodec;
}
public CaptureMode getCaptureMode() { public CaptureMode getCaptureMode() {
return captureMode; return captureMode;
} }
@@ -318,5 +328,6 @@ public class CastSettings implements Serializable {
streamProtection = other.streamProtection; streamProtection = other.streamProtection;
receiverDisplayResolution = other.receiverDisplayResolution; receiverDisplayResolution = other.receiverDisplayResolution;
negotiatedVideoMime = other.negotiatedVideoMime; negotiatedVideoMime = other.negotiatedVideoMime;
resolvedAudioCodec = other.resolvedAudioCodec;
} }
} }

View File

@@ -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());
}
}

View File

@@ -17,6 +17,7 @@ import android.util.Log;
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.sender.CodecCatalog; import com.foxx.androidcast.sender.CodecCatalog;
import org.json.JSONObject; import org.json.JSONObject;
@@ -248,10 +249,10 @@ public final class CodecPriorityCatalog {
public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) { public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) {
if (preference == CastSettings.AudioCodec.SPEEX) { if (preference == CastSettings.AudioCodec.SPEEX) {
return NativeCodecBridge.isSpeexAvailable() ? CodecTier.SOFTWARE : CodecTier.PASSTHROUGH; return NativeCodecBridge.isSpeexAvailable() ? CodecTier.HARDWARE : CodecTier.PASSTHROUGH;
} }
if (preference == CastSettings.AudioCodec.OPUS) { 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) { if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) {
return CodecTier.PASSTHROUGH; return CodecTier.PASSTHROUGH;
@@ -259,6 +260,103 @@ public final class CodecPriorityCatalog {
return CodecTier.HARDWARE; 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<AudioNegotiationCandidate> 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<AudioNegotiationCandidate> candidates;
AudioNegotiationDecision(AudioNegotiationCandidate selected,
List<AudioNegotiationCandidate> candidates) {
this.codec = selected.codec;
this.score = selected.score;
this.candidates = candidates;
}
String logLine() {
return codec.name() + " score=" + score;
}
}
public static <T> List<T> sortByPriority(List<ScoredItem<T>> items) { public static <T> List<T> sortByPriority(List<ScoredItem<T>> items) {
List<ScoredItem<T>> copy = new ArrayList<>(items); List<ScoredItem<T>> copy = new ArrayList<>(items);
Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore)); Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore));

View File

@@ -12,12 +12,19 @@ package com.foxx.androidcast.media.codec;
* Digest: SHA256 e8b2c92d5f3a97150adcf9af3f44791d2ee6a0db31dd7b7d96cb970bedc5e9fd * Digest: SHA256 e8b2c92d5f3a97150adcf9af3f44791d2ee6a0db31dd7b7d96cb970bedc5e9fd
**********************************************************************/ **********************************************************************/
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.AudioNegotiator;
import com.foxx.androidcast.sender.AudioEncoder; import com.foxx.androidcast.sender.AudioEncoder;
/** Creates audio encoder sinks. */ /** Creates audio encoder sinks. */
public final class AudioCodecFactory { public final class AudioCodecFactory {
private 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) { public static AudioEncoderSink createEncoder(CastSettings.AudioCodec preference) {
CodecSessionRegistry.setAudioPreference(preference); CodecSessionRegistry.setAudioPreference(preference);
AudioCodecBackend backend = PassthroughCodecPolicy.audioBackendFor(preference, true); AudioCodecBackend backend = PassthroughCodecPolicy.audioBackendFor(preference, true);

View File

@@ -164,7 +164,7 @@ public final class PassthroughCodecPolicy {
return "Passthrough (debug)"; return "Passthrough (debug)";
case AUTO: case AUTO:
default: default:
return "AUTO (AAC)"; return "AUTO";
} }
} }
} }

View File

@@ -13,6 +13,7 @@ package com.foxx.androidcast.network;
**********************************************************************/ **********************************************************************/
import android.media.MediaFormat; import android.media.MediaFormat;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.sender.CodecCatalog; import com.foxx.androidcast.sender.CodecCatalog;
@@ -71,4 +72,51 @@ public final class CastCodecFlags {
} }
return mask; 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;
}
}
} }

View File

@@ -744,16 +744,18 @@ public class ReceiverCastService extends Service {
: android.media.MediaFormat.MIMETYPE_VIDEO_AVC; : android.media.MediaFormat.MIMETYPE_VIDEO_AVC;
CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime); CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime);
if (remoteCastSettings != null) { if (remoteCastSettings != null) {
CastSettings.AudioCodec effectiveAudio =
com.foxx.androidcast.media.AudioNegotiator.effective(remoteCastSettings);
CodecSessionRegistry.setVideoPreference(remoteCastSettings.getVideoCodec()); CodecSessionRegistry.setVideoPreference(remoteCastSettings.getVideoCodec());
CodecSessionRegistry.setAudioPreference(remoteCastSettings.getAudioCodec()); CodecSessionRegistry.setAudioPreference(effectiveAudio);
CodecSessionRegistry.setAudioBackend( CodecSessionRegistry.setAudioBackend(
PassthroughCodecPolicy.audioBackendFor( PassthroughCodecPolicy.audioBackendFor(
remoteCastSettings.getAudioCodec(), effectiveAudio,
remoteCastSettings.isAudioRequested())); remoteCastSettings.isAudioRequested()));
CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription( CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription(
remoteCastSettings.getStreamProtection(), remoteCastSettings.getStreamProtection(),
remoteCastSettings.getTransport(), remoteCastSettings.getTransport(),
remoteCastSettings.getAudioCodec())); effectiveAudio));
} }
videoDecoder.configure(mime, pendingWidth, pendingHeight, pendingCsd0, pendingCsd1, attachedSurface, videoDecoder.configure(mime, pendingWidth, pendingHeight, pendingCsd0, pendingCsd1, attachedSurface,
new VideoDecoderSink.Listener() { new VideoDecoderSink.Listener() {

View File

@@ -91,11 +91,10 @@ public final class CodecCatalog {
boolean opus = NativeCodecBridge.isOpusAvailable(); boolean opus = NativeCodecBridge.isOpusAvailable();
boolean speex = NativeCodecBridge.isSpeexAvailable(); boolean speex = NativeCodecBridge.isSpeexAvailable();
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0)); 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.OPUS, opus, settings));
out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings));
out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, speex, settings)); out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, speex, settings));
out.add(scoredAudio(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, true, settings)); out.add(scoredAudio(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, true, settings));
sortAudioChoices(out);
return out; return out;
} }

View File

@@ -16,6 +16,7 @@ import android.util.Log;
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.DeviceInfo; import com.foxx.androidcast.DeviceInfo;
import com.foxx.androidcast.media.AudioNegotiator;
import com.foxx.androidcast.network.CastCodecFlags; import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.network.CastFramingContext; import com.foxx.androidcast.network.CastFramingContext;
import com.foxx.androidcast.network.CastProtocol; import com.foxx.androidcast.network.CastProtocol;
@@ -113,8 +114,10 @@ public final class MultiCastCoordinator {
connectWithRetry(session, t.host, t.port); connectWithRetry(session, t.host, t.port);
CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings); CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement); applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement);
AudioNegotiator.resolveForSender(peerSettings, t.audioCodecs);
peers.add(new ActivePeer(t, session, hs.settings, hs.negotiatedVideoMime)); peers.add(new ActivePeer(t, session, hs.settings, hs.negotiatedVideoMime));
Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime
+ " audio=" + peerSettings.getResolvedAudioCodec()
+ " score=" + " score="
+ com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime( + com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime(
hs.negotiatedVideoMime, hs.settings)); hs.negotiatedVideoMime, hs.settings));
@@ -142,6 +145,7 @@ public final class MultiCastCoordinator {
connectWithRetry(session, target.host, target.port); connectWithRetry(session, target.host, target.port);
CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings); CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement); applyNegotiatedProtection(session, peerSettings, hs.receiverAdvertisement);
AudioNegotiator.resolveForSender(peerSettings, target.audioCodecs);
peers.add(new ActivePeer(target, session, hs.settings, hs.negotiatedVideoMime)); peers.add(new ActivePeer(target, session, hs.settings, hs.negotiatedVideoMime));
if (keyframeRequester != null) { if (keyframeRequester != null) {
keyframeRequester.requestKeyframe(); keyframeRequester.requestKeyframe();

View File

@@ -55,6 +55,7 @@ import com.foxx.androidcast.network.control.PassThroughNetworkControlPlane;
import com.foxx.androidcast.network.transport.TransportLossStats; import com.foxx.androidcast.network.transport.TransportLossStats;
import com.foxx.androidcast.network.transport.TransportLossStatsBridge; import com.foxx.androidcast.network.transport.TransportLossStatsBridge;
import com.foxx.androidcast.network.selftest.NetworkSelfTestBridge; 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.AudioCodecFactory;
import com.foxx.androidcast.media.codec.CodecSessionRegistry; import com.foxx.androidcast.media.codec.CodecSessionRegistry;
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy; import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
@@ -274,6 +275,7 @@ public class ScreenCastService extends Service implements
session = primary.session; session = primary.session;
settings = primary.settings; settings = primary.settings;
settings.setNegotiatedVideoMime(primary.negotiatedMime); settings.setNegotiatedVideoMime(primary.negotiatedMime);
AudioNegotiator.resolveForSender(settings, primary.target.audioCodecs);
final CastSettings captureSettings = settings; final CastSettings captureSettings = settings;
updateStatus(getString(R.string.authenticating)); updateStatus(getString(R.string.authenticating));
networkFeedback = new NetworkFeedbackManager(session, true); networkFeedback = new NetworkFeedbackManager(session, true);
@@ -303,7 +305,9 @@ public class ScreenCastService extends Service implements
} }
sessionStatsRecorder.mergeSettings(settings); sessionStatsRecorder.mergeSettings(settings);
sessionStatsRecorder.setClientCount(peers.size()); sessionStatsRecorder.setClientCount(peers.size());
sessionStatsRecorder.setCodecs(settings.getNegotiatedVideoMime(), "AAC"); sessionStatsRecorder.setCodecs(settings.getNegotiatedVideoMime(),
PassthroughCodecPolicy.displayAudioPreference(
AudioNegotiator.effective(settings)));
} }
final int projResultCode = resultCode; final int projResultCode = resultCode;
final Intent projData = data != null ? new Intent(data) : null; final Intent projData = data != null ? new Intent(data) : null;
@@ -806,9 +810,10 @@ public class ScreenCastService extends Service implements
if (audioCapture != null) { if (audioCapture != null) {
audioCapture.stop(); audioCapture.stop();
} }
audioEncoder = AudioCodecFactory.createEncoder(settings.getAudioCodec()); CastSettings.AudioCodec effectiveAudio = AudioNegotiator.effective(settings);
audioEncoder = AudioCodecFactory.createEncoder(settings);
CodecSessionRegistry.setAudioBackend( CodecSessionRegistry.setAudioBackend(
PassthroughCodecPolicy.audioBackendFor(settings.getAudioCodec(), true)); PassthroughCodecPolicy.audioBackendFor(effectiveAudio, true));
audioEncoder.prepare(new AudioEncoderSink.Callback() { audioEncoder.prepare(new AudioEncoderSink.Callback() {
private boolean configSent; private boolean configSent;

View File

@@ -233,7 +233,7 @@
<string name="codec_speex">Speex</string> <string name="codec_speex">Speex</string>
<string name="codec_opus">Opus</string> <string name="codec_opus">Opus</string>
<string name="codec_audio_passthrough_debug">Passthrough (отладка)</string> <string name="codec_audio_passthrough_debug">Passthrough (отладка)</string>
<string name="codec_audio_auto">AUTO (AAC)</string> <string name="codec_audio_auto">AUTO</string>
<string name="pip_enter">Картинка в картинке (PiP)</string> <string name="pip_enter">Картинка в картинке (PiP)</string>
<string name="pip_unavailable">Режим «картинка в картинке» (PiP) недоступен на этом устройстве</string> <string name="pip_unavailable">Режим «картинка в картинке» (PiP) недоступен на этом устройстве</string>
<string name="pip_disabled_title">PiP отключено</string> <string name="pip_disabled_title">PiP отключено</string>

View File

@@ -325,7 +325,7 @@
<string name="codec_speex">Speex</string> <string name="codec_speex">Speex</string>
<string name="codec_opus">Opus</string> <string name="codec_opus">Opus</string>
<string name="codec_audio_passthrough_debug">Passthrough (debug)</string> <string name="codec_audio_passthrough_debug">Passthrough (debug)</string>
<string name="codec_audio_auto">AUTO (AAC)</string> <string name="codec_audio_auto">AUTO</string>
<string name="pip_enter">Picture-in-picture</string> <string name="pip_enter">Picture-in-picture</string>
<string name="pip_unavailable">Picture-in-picture is not available on this device</string> <string name="pip_unavailable">Picture-in-picture is not available on this device</string>
<string name="pip_disabled_title">Picture-in-picture disabled</string> <string name="pip_disabled_title">Picture-in-picture disabled</string>

View File

@@ -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]);
}
}