diff --git a/app/src/main/java/com/foxx/androidcast/AppPreferences.java b/app/src/main/java/com/foxx/androidcast/AppPreferences.java index 62fe7c9..bf7cdad 100644 --- a/app/src/main/java/com/foxx/androidcast/AppPreferences.java +++ b/app/src/main/java/com/foxx/androidcast/AppPreferences.java @@ -20,6 +20,7 @@ public final class AppPreferences { private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio"; private static final String KEY_DEV_RECEIVER_DEBUG_OVERLAY = "dev_receiver_debug_overlay"; private static final String KEY_DEV_GRAB_SESSION_STATS = "dev_grab_session_stats"; + private static final String KEY_DEV_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores"; private static final String KEY_RECEIVER_PIP_USER_DEFINED = "receiver_pip_user_defined"; private static final String KEY_RECEIVER_PIP_ENABLED = "receiver_pip_enabled"; @@ -114,6 +115,21 @@ public final class AppPreferences { prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply(); } + /** + * Show priority score suffix in codec settings spinners (also on when receiver debug overlay is on). + */ + public static boolean isShowCodecPriorityScores(Context context) { + return isShowReceiverDebugOverlay(context) || isDevShowCodecPriorityScoresExplicit(context); + } + + public static boolean isDevShowCodecPriorityScoresExplicit(Context context) { + return prefs(context).getBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, false); + } + + public static void setShowCodecPriorityScores(Context context, boolean show) { + prefs(context).edit().putBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, show).apply(); + } + /** * Receiver PiP: user toggle when set; otherwise derived from PiP support + notifications permission. */ diff --git a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java index 0ba057a..ba06f25 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java @@ -183,7 +183,7 @@ public final class CastSettingsBinder { boolean[] selectable = new boolean[choices.size()]; for (int i = 0; i < choices.size(); i++) { values[i] = choices.get(i).codec; - labels[i] = labelVideoCodec(activity, choices.get(i).codec); + labels[i] = labelVideoCodec(activity, choices.get(i).codec, choices.get(i).priorityScore); selectable[i] = choices.get(i).selectable; } CastSettings.VideoCodec current = settings.getVideoCodec(); @@ -194,6 +194,14 @@ public final class CastSettingsBinder { SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setVideoCodec); } + /** Rebind codec spinners after {@code codecs.json} reload or debug score toggle. */ + public static void refreshCodecSpinners(AppCompatActivity activity, CastSettings senderSettings, + CastSettings receiverSettings) { + bindVideoCodec(activity, senderSettings, R.id.spinner_sender_video_codec); + bindVideoCodec(activity, receiverSettings, R.id.spinner_receiver_video_codec); + bindAudioCodec(activity, senderSettings); + } + private static boolean isVideoCodecSelectable(List choices, CastSettings.VideoCodec codec) { for (CodecCatalog.VideoCodecChoice c : choices) { @@ -221,7 +229,7 @@ public final class CastSettingsBinder { boolean[] selectable = new boolean[choices.size()]; for (int i = 0; i < choices.size(); i++) { values[i] = choices.get(i).codec; - labels[i] = labelAudioCodec(activity, choices.get(i).codec); + labels[i] = labelAudioCodec(activity, choices.get(i).codec, choices.get(i).priorityScore); selectable[i] = choices.get(i).selectable; } CastSettings.AudioCodec current = settings.getAudioCodec(); @@ -552,35 +560,57 @@ public final class CastSettingsBinder { } } - private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec) { + private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec, + int priorityScore) { + String base; switch (codec) { case H264: - return activity.getString(R.string.codec_h264); + base = activity.getString(R.string.codec_h264); + break; case H265: - return activity.getString(R.string.codec_h265); + base = activity.getString(R.string.codec_h265); + break; case LIBVPX_VP8: - return activity.getString(R.string.codec_libvpx_vp8); + base = activity.getString(R.string.codec_libvpx_vp8); + break; case LIBVPX_VP9: - return activity.getString(R.string.codec_libvpx_vp9); + base = activity.getString(R.string.codec_libvpx_vp9); + break; case PASSTHROUGH: - return activity.getString(R.string.codec_passthrough); + base = activity.getString(R.string.codec_passthrough); + break; case AUTO: default: return activity.getString(R.string.option_auto); } + return appendPriorityScore(activity, base, codec != CastSettings.VideoCodec.AUTO, priorityScore); } - private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec) { + private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec, + int priorityScore) { + String base; switch (codec) { case AAC: - return activity.getString(R.string.codec_aac); + base = activity.getString(R.string.codec_aac); + break; case SPEEX: - return activity.getString(R.string.codec_speex); + base = activity.getString(R.string.codec_speex); + break; case OPUS: - return activity.getString(R.string.codec_opus); + base = activity.getString(R.string.codec_opus); + break; case AUTO: default: return activity.getString(R.string.codec_audio_auto); } + return appendPriorityScore(activity, base, codec != CastSettings.AudioCodec.AUTO, priorityScore); + } + + private static String appendPriorityScore(AppCompatActivity activity, String base, boolean showScore, + int priorityScore) { + if (!showScore || !AppPreferences.isShowCodecPriorityScores(activity)) { + return base; + } + return base + activity.getString(R.string.codec_priority_score_suffix, priorityScore); } } diff --git a/app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java b/app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java index bb22036..d4d392c 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java @@ -122,7 +122,9 @@ public class CastSettingsFragment extends Fragment { @Override public void onResume() { super.onResume(); - CastSettingsBinder.refreshReceiverPipFromPermissions((AppCompatActivity) requireActivity()); + AppCompatActivity activity = (AppCompatActivity) requireActivity(); + CastSettingsBinder.refreshReceiverPipFromPermissions(activity); + CastSettingsBinder.refreshCodecSpinners(activity, senderSettings, receiverSettings); } @Override diff --git a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java index 6614eff..dac3756 100644 --- a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java +++ b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java @@ -2,10 +2,14 @@ package com.foxx.androidcast; import android.content.Context; import android.os.Bundle; +import android.widget.Button; import android.widget.CheckBox; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; +import com.foxx.androidcast.media.CodecPriorityCatalog; + /** Hidden developer settings (opened via version label easter egg). */ public class DeveloperSettingsActivity extends AppCompatActivity { @Override @@ -21,14 +25,24 @@ public class DeveloperSettingsActivity extends AppCompatActivity { setTitle(R.string.developer_settings_title); CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay); + CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores); CheckBox sessionStats = findViewById(R.id.check_grab_session_stats); + Button reloadCodecs = findViewById(R.id.btn_reload_codecs_json); debugOverlay.setChecked(AppPreferences.isShowReceiverDebugOverlay(this)); + codecScores.setChecked(AppPreferences.isDevShowCodecPriorityScoresExplicit(this)); sessionStats.setChecked(AppPreferences.isGrabSessionStats(this)); debugOverlay.setOnCheckedChangeListener((buttonView, isChecked) -> AppPreferences.setShowReceiverDebugOverlay(this, isChecked)); + codecScores.setOnCheckedChangeListener((buttonView, isChecked) -> + AppPreferences.setShowCodecPriorityScores(this, isChecked)); sessionStats.setOnCheckedChangeListener((buttonView, isChecked) -> AppPreferences.setGrabSessionStats(this, isChecked)); + + reloadCodecs.setOnClickListener(v -> { + String summary = CodecPriorityCatalog.reload(this); + Toast.makeText(this, summary, Toast.LENGTH_LONG).show(); + }); } } diff --git a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java index 49bf06f..0cc7668 100644 --- a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java +++ b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java @@ -112,7 +112,7 @@ public final class CastDiagnosticsLabels { ? "libvpx VP9" : "libvpx VP9 (HW fallback)"; case AUTO: default: - return "CodecNegotiator (HEVC→AVC→VP9→VP8)"; + return "AUTO (lowest priority score wins)"; } } diff --git a/app/src/main/java/com/foxx/androidcast/media/CodecNegotiator.java b/app/src/main/java/com/foxx/androidcast/media/CodecNegotiator.java index 4d13905..c1c533b 100644 --- a/app/src/main/java/com/foxx/androidcast/media/CodecNegotiator.java +++ b/app/src/main/java/com/foxx/androidcast/media/CodecNegotiator.java @@ -1,9 +1,9 @@ package com.foxx.androidcast.media; import android.media.MediaFormat; +import android.util.Log; import com.foxx.androidcast.CastSettings; -import com.foxx.androidcast.sender.CodecCatalog; import java.util.HashSet; import java.util.List; @@ -12,39 +12,61 @@ import java.util.Set; /** Picks a video MIME type both sides support, honoring user preference when possible. */ public final class CodecNegotiator { + private static final String TAG = "CodecNegotiator"; + private CodecNegotiator() {} public static String negotiate(List senderEncoders, List receiverDecoders, CastSettings.VideoCodec preference) { - return negotiate(senderEncoders, receiverDecoders, preference, null); + return negotiate(senderEncoders, receiverDecoders, preference, null, "negotiate"); } public static String negotiate(List senderEncoders, List receiverDecoders, CastSettings.VideoCodec preference, CastSettings negotiationSettings) { + return negotiate(senderEncoders, receiverDecoders, preference, negotiationSettings, "receiver"); + } + + public static String negotiate(List senderEncoders, List receiverDecoders, + CastSettings.VideoCodec preference, CastSettings negotiationSettings, String logRole) { Set enc = new HashSet<>(senderEncoders); Set dec = new HashSet<>(receiverDecoders); CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings(); if (preference == CastSettings.VideoCodec.H264) { - return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec); + String mime = pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec); + CodecPriorityCatalog.logExplicitNegotiation(logRole, mime, + CastSettings.VideoCodec.H264, scoreSettings); + return mime; } if (preference == CastSettings.VideoCodec.H265) { - return pick(MediaFormat.MIMETYPE_VIDEO_HEVC, enc, dec); + String mime = pick(MediaFormat.MIMETYPE_VIDEO_HEVC, enc, dec); + CodecPriorityCatalog.logExplicitNegotiation(logRole, mime, + CastSettings.VideoCodec.H265, scoreSettings); + return mime; } if (preference == CastSettings.VideoCodec.LIBVPX_VP8) { - return pick(MediaFormat.MIMETYPE_VIDEO_VP8, enc, dec); + String mime = pick(MediaFormat.MIMETYPE_VIDEO_VP8, enc, dec); + CodecPriorityCatalog.logExplicitNegotiation(logRole, mime, + CastSettings.VideoCodec.LIBVPX_VP8, scoreSettings); + return mime; } if (preference == CastSettings.VideoCodec.LIBVPX_VP9) { - return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec); + String mime = pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec); + CodecPriorityCatalog.logExplicitNegotiation(logRole, mime, + CastSettings.VideoCodec.LIBVPX_VP9, scoreSettings); + return mime; } - String best = CodecPriorityCatalog.negotiateBestVideoMime( + CodecPriorityCatalog.NegotiationDecision decision = CodecPriorityCatalog.decideBestVideoMime( senderEncoders, receiverDecoders, scoreSettings); - if (best != null) { - return best; + if (decision != null) { + CodecPriorityCatalog.logNegotiationDecision(logRole, decision); + return decision.mime; } if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { - return MediaFormat.MIMETYPE_VIDEO_AVC; + String mime = MediaFormat.MIMETYPE_VIDEO_AVC; + Log.w(TAG, logRole + " codec fallback H.264 (no scored candidate)"); + return mime; } throw new IllegalStateException("No common video codec (sender=" + enc + " receiver=" + dec + ")"); } 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 659e263..fdb1b71 100644 --- a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java @@ -19,6 +19,8 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import androidx.annotation.Nullable; + /** * Runtime codec priority: tier base (0 / 500 / 1000) + component hooks. * Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering. @@ -43,16 +45,34 @@ public final class CodecPriorityCatalog { rootJson = loadJson(appContext); } + /** Reload {@code assets/codecs.json} and refresh local codec capability cache. */ + public static String reload(Context context) { + Context app = context.getApplicationContext(); + appContext = app; + JSONObject loaded = loadJson(app); + rootJson = loaded; + CodecCatalog.invalidateCapabilities(); + int overrides = countJsonOverrides(loaded); + String summary = "codecs.json reloaded (" + overrides + " override field(s))"; + Log.i(TAG, summary); + return summary; + } + public static int priorityScore(CodecPriorityId id, CodecTier tier, CastSettings settings) { + return breakdown(id, tier, settings).total; + } + + public static ScoreBreakdown breakdown(CodecPriorityId id, CodecTier tier, CastSettings settings) { if (id == null || tier == null) { - return Integer.MAX_VALUE; + return new ScoreBreakdown(Integer.MAX_VALUE, 0, 0, 0, 0, 0, 0); } - return tier.baseScore - + component(id, KEY_INTERNAL, defaultInternal(id)) - + component(id, KEY_FEC, defaultFec(id)) - + component(id, KEY_PROTECTION, defaultProtection(id, settings)) - + component(id, KEY_OVERRIDE, 0) - + component(id, KEY_BITRATE, defaultBitrate(id)); + int internal = component(id, KEY_INTERNAL, defaultInternal(id)); + int fec = component(id, KEY_FEC, defaultFec(id)); + int protection = component(id, KEY_PROTECTION, defaultProtection(id, settings)); + int override = component(id, KEY_OVERRIDE, 0); + int bitrate = component(id, KEY_BITRATE, defaultBitrate(id)); + int total = tier.baseScore + internal + fec + protection + override + bitrate; + return new ScoreBreakdown(total, tier.baseScore, internal, fec, protection, override, bitrate); } public static int priorityScoreForVideoMime(String mime, CodecTier tier, CastSettings settings) { @@ -82,24 +102,77 @@ public final class CodecPriorityCatalog { } /** Best MIME for AUTO: lowest score among codecs both sides can use. */ + @Nullable public static String negotiateBestVideoMime(List senderEncoders, List receiverDecoders, CastSettings settings) { + NegotiationDecision decision = decideBestVideoMime(senderEncoders, receiverDecoders, settings); + return decision != null ? decision.mime : null; + } + + /** AUTO negotiation with ranked candidates for logging and diagnostics. */ + @Nullable + public static NegotiationDecision decideBestVideoMime(List senderEncoders, + List receiverDecoders, CastSettings settings) { List common = commonMimes(senderEncoders, receiverDecoders); if (common.isEmpty()) { return null; } - String best = null; - int bestScore = Integer.MAX_VALUE; + List candidates = new ArrayList<>(); for (String mime : common) { - int score = Math.max( - videoMimeScoreOnDevice(mime, false, settings), - assumedRemoteEncoderScore(mime, settings)); - if (score < bestScore) { - bestScore = score; - best = mime; - } + candidates.add(buildCandidate(mime, settings)); } - return best; + Collections.sort(candidates, Comparator.comparingInt(c -> c.combinedScore)); + NegotiationCandidate best = candidates.get(0); + return new NegotiationDecision(best, candidates); + } + + public static void logNegotiationDecision(String role, NegotiationDecision decision) { + if (decision == null) { + return; + } + Log.i(TAG, role + " codec AUTO: selected " + decision.logLine()); + for (NegotiationCandidate c : decision.candidates) { + boolean picked = c.mime != null && c.mime.equals(decision.mime); + Log.i(TAG, role + " codec candidate: " + c.logLine(picked)); + } + } + + public static void logExplicitNegotiation(String role, String mime, CastSettings.VideoCodec preference, + CastSettings settings) { + int score = combinedVideoMimeScore(mime, settings); + Log.i(TAG, role + " codec explicit " + preference.name() + ": " + + displayMime(mime) + " (" + mime + ") score=" + score + + " · " + formatVideoMimeBreakdown(mime, settings)); + } + + public static void logReceivedCodec(String role, String mime, CastSettings settings) { + int score = combinedVideoMimeScore(mime, settings); + Log.i(TAG, role + " codec selected by peer: " + + displayMime(mime) + " (" + mime + ") score=" + score + + " · " + formatVideoMimeBreakdown(mime, settings)); + } + + public static String formatVideoMimeBreakdown(String mime, CastSettings settings) { + CodecPriorityId id = CodecPriorityId.fromVideoMime(mime); + if (id == null) { + return mime + " unknown"; + } + CodecTier localDec = CodecCatalog.effectiveTierForVideoMime(mime, false); + ScoreBreakdown dec = breakdown(id, localDec, settings); + int peerEncScore = assumedRemoteEncoderScore(mime, settings); + return String.format(Locale.US, + "combined=%d dec=%d (tier %d +%d+%d+%d+%d+%d) peerEnc=%d", + Math.max(dec.total, peerEncScore), + dec.total, dec.tierBase, dec.internal, dec.fec, dec.protection, dec.override, dec.bitrate, + peerEncScore); + } + + private static NegotiationCandidate buildCandidate(String mime, CastSettings settings) { + int localDecoder = videoMimeScoreOnDevice(mime, false, settings); + int peerEncoder = assumedRemoteEncoderScore(mime, settings); + int combined = Math.max(localDecoder, peerEncoder); + return new NegotiationCandidate(mime, combined, localDecoder, peerEncoder, + formatVideoMimeBreakdown(mime, settings)); } /** Conservative score when sender and receiver may use different tiers for the same MIME. */ @@ -192,6 +265,88 @@ public final class CodecPriorityCatalog { } } + public static final class ScoreBreakdown { + public final int total; + public final int tierBase; + public final int internal; + public final int fec; + public final int protection; + public final int override; + public final int bitrate; + + ScoreBreakdown(int total, int tierBase, int internal, int fec, int protection, int override, + int bitrate) { + this.total = total; + this.tierBase = tierBase; + this.internal = internal; + this.fec = fec; + this.protection = protection; + this.override = override; + this.bitrate = bitrate; + } + } + + public static final class NegotiationCandidate { + public final String mime; + public final int combinedScore; + public final int localDecoderScore; + public final int peerEncoderScore; + public final String detail; + + NegotiationCandidate(String mime, int combinedScore, int localDecoderScore, int peerEncoderScore, + String detail) { + this.mime = mime; + this.combinedScore = combinedScore; + this.localDecoderScore = localDecoderScore; + this.peerEncoderScore = peerEncoderScore; + this.detail = detail; + } + + String logLine(boolean selected) { + return (selected ? "* " : " ") + + displayMime(mime) + " score=" + combinedScore + + " (dec=" + localDecoderScore + " peerEnc=" + peerEncoderScore + ") " + detail; + } + } + + public static final class NegotiationDecision { + public final String mime; + public final int score; + public final NegotiationCandidate selected; + public final List candidates; + + NegotiationDecision(NegotiationCandidate selected, List candidates) { + this.selected = selected; + this.candidates = candidates; + this.mime = selected.mime; + this.score = selected.combinedScore; + } + + String logLine() { + return displayMime(mime) + " (" + mime + ") score=" + score + " · " + selected.detail; + } + } + + private static String displayMime(String mime) { + if (mime == null) { + return "?"; + } + if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { + return "H.264"; + } + if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) { + return "H.265"; + } + if (MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime)) { + return "VP9"; + } + if (MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)) { + return "VP8"; + } + int slash = mime.lastIndexOf('/'); + return slash >= 0 ? mime.substring(slash + 1).toUpperCase(Locale.US) : mime; + } + private static List commonMimes(List enc, List dec) { List out = new ArrayList<>(); if (enc == null || dec == null) { @@ -336,6 +491,33 @@ public final class CodecPriorityCatalog { } } + private static int countJsonOverrides(JSONObject root) { + int n = 0; + n += countSectionOverrides(root.optJSONObject("video")); + n += countSectionOverrides(root.optJSONObject("audio")); + return n; + } + + private static int countSectionOverrides(JSONObject section) { + if (section == null) { + return 0; + } + int n = 0; + java.util.Iterator keys = section.keys(); + while (keys.hasNext()) { + JSONObject codec = section.optJSONObject(keys.next()); + if (codec == null) { + continue; + } + for (String k : new String[] {KEY_INTERNAL, KEY_FEC, KEY_PROTECTION, KEY_OVERRIDE, KEY_BITRATE}) { + if (codec.has(k)) { + n++; + } + } + } + return n; + } + private static JSONObject loadJson(Context context) { try (BufferedReader r = new BufferedReader(new InputStreamReader( context.getAssets().open(ASSET), StandardCharsets.UTF_8))) { diff --git a/app/src/main/java/com/foxx/androidcast/network/CastSession.java b/app/src/main/java/com/foxx/androidcast/network/CastSession.java index 67a01b5..3d582dc 100644 --- a/app/src/main/java/com/foxx/androidcast/network/CastSession.java +++ b/app/src/main/java/com/foxx/androidcast/network/CastSession.java @@ -2,6 +2,7 @@ package com.foxx.androidcast.network; import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.media.CodecNegotiator; +import com.foxx.androidcast.media.CodecPriorityCatalog; import com.foxx.androidcast.media.codec.PassthroughCodecPolicy; import com.foxx.androidcast.sender.CodecCatalog; @@ -74,6 +75,7 @@ public class CastSession { throw new IOException("Codec negotiation timeout"); } settings.setNegotiatedVideoMime(negotiated); + CodecPriorityCatalog.logReceivedCodec("sender", negotiated, settings); return new HandshakeResult(deviceName, settings, negotiated); } @@ -118,7 +120,7 @@ public class CastSession { List receiverCaps = CodecCatalog.localDecoderMimes(); transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps)); String mime = CodecNegotiator.negotiate(senderCaps, receiverCaps, - PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote); + PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote, "receiver"); transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime)); remote.setNegotiatedVideoMime(mime); return new HandshakeResult(senderName, remote, mime); 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 68d6c86..f1cdc73 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java @@ -172,6 +172,11 @@ public final class CodecCatalog { choices.addAll(rest); } + /** Clears cached HW/SW probe (call after {@link com.foxx.androidcast.media.CodecPriorityCatalog#reload}). */ + public static void invalidateCapabilities() { + capabilities = null; + } + private static void ensureCapabilities() { if (capabilities != null) { return; 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 4f5b5b0..8602fc1 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java +++ b/app/src/main/java/com/foxx/androidcast/sender/MultiCastCoordinator.java @@ -101,7 +101,10 @@ public final class MultiCastCoordinator { CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings); applyNegotiatedProtection(session, peerSettings, hs.settings); 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 + + " score=" + + com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime( + hs.negotiatedVideoMime, hs.settings)); } catch (IOException e) { last = e; Log.w(TAG, "Peer failed " + t.name + ": " + e.getMessage()); diff --git a/app/src/main/res/layout/activity_developer_settings.xml b/app/src/main/res/layout/activity_developer_settings.xml index 293771c..7066d3b 100644 --- a/app/src/main/res/layout/activity_developer_settings.xml +++ b/app/src/main/res/layout/activity_developer_settings.xml @@ -25,6 +25,13 @@ android:checked="true" android:text="@string/dev_show_receiver_debug_overlay" /> + + + +