mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:38:52 +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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<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) {
|
||||
List<ScoredItem<T>> copy = new ArrayList<>(items);
|
||||
Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -164,7 +164,7 @@ public final class PassthroughCodecPolicy {
|
||||
return "Passthrough (debug)";
|
||||
case AUTO:
|
||||
default:
|
||||
return "AUTO (AAC)";
|
||||
return "AUTO";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
<string name="codec_speex">Speex</string>
|
||||
<string name="codec_opus">Opus</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_unavailable">Режим «картинка в картинке» (PiP) недоступен на этом устройстве</string>
|
||||
<string name="pip_disabled_title">PiP отключено</string>
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
<string name="codec_speex">Speex</string>
|
||||
<string name="codec_opus">Opus</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_unavailable">Picture-in-picture is not available on this device</string>
|
||||
<string name="pip_disabled_title">Picture-in-picture disabled</string>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user