mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:58:14 +03:00
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<CodecCatalog.VideoCodecChoice> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> senderEncoders, List<String> receiverDecoders,
|
||||
CastSettings.VideoCodec preference) {
|
||||
return negotiate(senderEncoders, receiverDecoders, preference, null);
|
||||
return negotiate(senderEncoders, receiverDecoders, preference, null, "negotiate");
|
||||
}
|
||||
|
||||
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||
CastSettings.VideoCodec preference, CastSettings negotiationSettings) {
|
||||
return negotiate(senderEncoders, receiverDecoders, preference, negotiationSettings, "receiver");
|
||||
}
|
||||
|
||||
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||
CastSettings.VideoCodec preference, CastSettings negotiationSettings, String logRole) {
|
||||
Set<String> enc = new HashSet<>(senderEncoders);
|
||||
Set<String> 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 + ")");
|
||||
}
|
||||
|
||||
@@ -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<String> senderEncoders, List<String> 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<String> senderEncoders,
|
||||
List<String> receiverDecoders, CastSettings settings) {
|
||||
List<String> common = commonMimes(senderEncoders, receiverDecoders);
|
||||
if (common.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String best = null;
|
||||
int bestScore = Integer.MAX_VALUE;
|
||||
List<NegotiationCandidate> 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<NegotiationCandidate> candidates;
|
||||
|
||||
NegotiationDecision(NegotiationCandidate selected, List<NegotiationCandidate> 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<String> commonMimes(List<String> enc, List<String> dec) {
|
||||
List<String> 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<String> 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))) {
|
||||
|
||||
@@ -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<String> 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
android:checked="true"
|
||||
android:text="@string/dev_show_receiver_debug_overlay" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_show_codec_priority_scores"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/dev_show_codec_priority_scores" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_grab_session_stats"
|
||||
android:layout_width="match_parent"
|
||||
@@ -32,5 +39,19 @@
|
||||
android:layout_marginTop="12dp"
|
||||
android:checked="true"
|
||||
android:text="@string/dev_grab_session_stats" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_reload_codecs_json"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/dev_reload_codecs_json" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/dev_reload_codecs_json_hint"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -187,7 +187,11 @@
|
||||
<string name="licenses_load_error">Не могу найти факл лицензий</string>
|
||||
<string name="developer_settings_title">Для разработчиков</string>
|
||||
<string name="dev_show_receiver_debug_overlay">Показывать отладочный оверлей на приёмнике</string>
|
||||
<string name="dev_show_codec_priority_scores">Показывать приоритет кодеков в настройках</string>
|
||||
<string name="dev_grab_session_stats">Сохранять анонимную статистику сессий</string>
|
||||
<string name="dev_reload_codecs_json">Перезагрузить codecs.json</string>
|
||||
<string name="dev_reload_codecs_json_hint">Подгружает overrides из assets/codecs.json без перезапуска. Откройте настройки снова, чтобы обновить списки.</string>
|
||||
<string name="codec_priority_score_suffix"> · %1$d</string>
|
||||
<string name="label_audio_codec">Кодек для звука</string>
|
||||
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
||||
<string name="codec_libvpx_vp9">libvpx (VP9)</string>
|
||||
|
||||
@@ -187,7 +187,11 @@
|
||||
<string name="licenses_load_error">Could not load license text.</string>
|
||||
<string name="developer_settings_title">Developer settings</string>
|
||||
<string name="dev_show_receiver_debug_overlay">Show debug overlay on receiver</string>
|
||||
<string name="dev_show_codec_priority_scores">Show codec priority scores in settings</string>
|
||||
<string name="dev_grab_session_stats">Grab anonymous session stats</string>
|
||||
<string name="dev_reload_codecs_json">Reload codecs.json</string>
|
||||
<string name="dev_reload_codecs_json_hint">Loads assets/codecs.json overrides without restarting the app. Reopen the settings drawer to refresh spinners.</string>
|
||||
<string name="codec_priority_score_suffix"> · %1$d</string>
|
||||
<string name="label_audio_codec">Voice codec</string>
|
||||
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
||||
<string name="codec_libvpx_vp9">libvpx (VP9)</string>
|
||||
|
||||
Reference in New Issue
Block a user