mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:18:42 +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_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_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_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_USER_DEFINED = "receiver_pip_user_defined";
|
||||||
private static final String KEY_RECEIVER_PIP_ENABLED = "receiver_pip_enabled";
|
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();
|
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.
|
* 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()];
|
boolean[] selectable = new boolean[choices.size()];
|
||||||
for (int i = 0; i < choices.size(); i++) {
|
for (int i = 0; i < choices.size(); i++) {
|
||||||
values[i] = choices.get(i).codec;
|
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;
|
selectable[i] = choices.get(i).selectable;
|
||||||
}
|
}
|
||||||
CastSettings.VideoCodec current = settings.getVideoCodec();
|
CastSettings.VideoCodec current = settings.getVideoCodec();
|
||||||
@@ -194,6 +194,14 @@ public final class CastSettingsBinder {
|
|||||||
SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setVideoCodec);
|
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,
|
private static boolean isVideoCodecSelectable(List<CodecCatalog.VideoCodecChoice> choices,
|
||||||
CastSettings.VideoCodec codec) {
|
CastSettings.VideoCodec codec) {
|
||||||
for (CodecCatalog.VideoCodecChoice c : choices) {
|
for (CodecCatalog.VideoCodecChoice c : choices) {
|
||||||
@@ -221,7 +229,7 @@ public final class CastSettingsBinder {
|
|||||||
boolean[] selectable = new boolean[choices.size()];
|
boolean[] selectable = new boolean[choices.size()];
|
||||||
for (int i = 0; i < choices.size(); i++) {
|
for (int i = 0; i < choices.size(); i++) {
|
||||||
values[i] = choices.get(i).codec;
|
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;
|
selectable[i] = choices.get(i).selectable;
|
||||||
}
|
}
|
||||||
CastSettings.AudioCodec current = settings.getAudioCodec();
|
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) {
|
switch (codec) {
|
||||||
case H264:
|
case H264:
|
||||||
return activity.getString(R.string.codec_h264);
|
base = activity.getString(R.string.codec_h264);
|
||||||
|
break;
|
||||||
case H265:
|
case H265:
|
||||||
return activity.getString(R.string.codec_h265);
|
base = activity.getString(R.string.codec_h265);
|
||||||
|
break;
|
||||||
case LIBVPX_VP8:
|
case LIBVPX_VP8:
|
||||||
return activity.getString(R.string.codec_libvpx_vp8);
|
base = activity.getString(R.string.codec_libvpx_vp8);
|
||||||
|
break;
|
||||||
case LIBVPX_VP9:
|
case LIBVPX_VP9:
|
||||||
return activity.getString(R.string.codec_libvpx_vp9);
|
base = activity.getString(R.string.codec_libvpx_vp9);
|
||||||
|
break;
|
||||||
case PASSTHROUGH:
|
case PASSTHROUGH:
|
||||||
return activity.getString(R.string.codec_passthrough);
|
base = activity.getString(R.string.codec_passthrough);
|
||||||
|
break;
|
||||||
case AUTO:
|
case AUTO:
|
||||||
default:
|
default:
|
||||||
return activity.getString(R.string.option_auto);
|
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) {
|
switch (codec) {
|
||||||
case AAC:
|
case AAC:
|
||||||
return activity.getString(R.string.codec_aac);
|
base = activity.getString(R.string.codec_aac);
|
||||||
|
break;
|
||||||
case SPEEX:
|
case SPEEX:
|
||||||
return activity.getString(R.string.codec_speex);
|
base = activity.getString(R.string.codec_speex);
|
||||||
|
break;
|
||||||
case OPUS:
|
case OPUS:
|
||||||
return activity.getString(R.string.codec_opus);
|
base = activity.getString(R.string.codec_opus);
|
||||||
|
break;
|
||||||
case AUTO:
|
case AUTO:
|
||||||
default:
|
default:
|
||||||
return activity.getString(R.string.codec_audio_auto);
|
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
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
CastSettingsBinder.refreshReceiverPipFromPermissions((AppCompatActivity) requireActivity());
|
AppCompatActivity activity = (AppCompatActivity) requireActivity();
|
||||||
|
CastSettingsBinder.refreshReceiverPipFromPermissions(activity);
|
||||||
|
CastSettingsBinder.refreshCodecSpinners(activity, senderSettings, receiverSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package com.foxx.androidcast;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||||
|
|
||||||
/** Hidden developer settings (opened via version label easter egg). */
|
/** Hidden developer settings (opened via version label easter egg). */
|
||||||
public class DeveloperSettingsActivity extends AppCompatActivity {
|
public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
@@ -21,14 +25,24 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
|||||||
setTitle(R.string.developer_settings_title);
|
setTitle(R.string.developer_settings_title);
|
||||||
|
|
||||||
CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay);
|
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);
|
CheckBox sessionStats = findViewById(R.id.check_grab_session_stats);
|
||||||
|
Button reloadCodecs = findViewById(R.id.btn_reload_codecs_json);
|
||||||
|
|
||||||
debugOverlay.setChecked(AppPreferences.isShowReceiverDebugOverlay(this));
|
debugOverlay.setChecked(AppPreferences.isShowReceiverDebugOverlay(this));
|
||||||
|
codecScores.setChecked(AppPreferences.isDevShowCodecPriorityScoresExplicit(this));
|
||||||
sessionStats.setChecked(AppPreferences.isGrabSessionStats(this));
|
sessionStats.setChecked(AppPreferences.isGrabSessionStats(this));
|
||||||
|
|
||||||
debugOverlay.setOnCheckedChangeListener((buttonView, isChecked) ->
|
debugOverlay.setOnCheckedChangeListener((buttonView, isChecked) ->
|
||||||
AppPreferences.setShowReceiverDebugOverlay(this, isChecked));
|
AppPreferences.setShowReceiverDebugOverlay(this, isChecked));
|
||||||
|
codecScores.setOnCheckedChangeListener((buttonView, isChecked) ->
|
||||||
|
AppPreferences.setShowCodecPriorityScores(this, isChecked));
|
||||||
sessionStats.setOnCheckedChangeListener((buttonView, isChecked) ->
|
sessionStats.setOnCheckedChangeListener((buttonView, isChecked) ->
|
||||||
AppPreferences.setGrabSessionStats(this, 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)";
|
? "libvpx VP9" : "libvpx VP9 (HW fallback)";
|
||||||
case AUTO:
|
case AUTO:
|
||||||
default:
|
default:
|
||||||
return "CodecNegotiator (HEVC→AVC→VP9→VP8)";
|
return "AUTO (lowest priority score wins)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.foxx.androidcast.media;
|
package com.foxx.androidcast.media;
|
||||||
|
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.sender.CodecCatalog;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
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. */
|
/** Picks a video MIME type both sides support, honoring user preference when possible. */
|
||||||
public final class CodecNegotiator {
|
public final class CodecNegotiator {
|
||||||
|
private static final String TAG = "CodecNegotiator";
|
||||||
|
|
||||||
private CodecNegotiator() {}
|
private CodecNegotiator() {}
|
||||||
|
|
||||||
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||||
CastSettings.VideoCodec preference) {
|
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,
|
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||||
CastSettings.VideoCodec preference, CastSettings negotiationSettings) {
|
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> enc = new HashSet<>(senderEncoders);
|
||||||
Set<String> dec = new HashSet<>(receiverDecoders);
|
Set<String> dec = new HashSet<>(receiverDecoders);
|
||||||
CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings();
|
CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings();
|
||||||
|
|
||||||
if (preference == CastSettings.VideoCodec.H264) {
|
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) {
|
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) {
|
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) {
|
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);
|
senderEncoders, receiverDecoders, scoreSettings);
|
||||||
if (best != null) {
|
if (decision != null) {
|
||||||
return best;
|
CodecPriorityCatalog.logNegotiationDecision(logRole, decision);
|
||||||
|
return decision.mime;
|
||||||
}
|
}
|
||||||
if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
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 + ")");
|
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.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
|
* Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
|
||||||
* Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
|
* Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
|
||||||
@@ -43,16 +45,34 @@ public final class CodecPriorityCatalog {
|
|||||||
rootJson = loadJson(appContext);
|
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) {
|
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) {
|
if (id == null || tier == null) {
|
||||||
return Integer.MAX_VALUE;
|
return new ScoreBreakdown(Integer.MAX_VALUE, 0, 0, 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
return tier.baseScore
|
int internal = component(id, KEY_INTERNAL, defaultInternal(id));
|
||||||
+ component(id, KEY_INTERNAL, defaultInternal(id))
|
int fec = component(id, KEY_FEC, defaultFec(id));
|
||||||
+ component(id, KEY_FEC, defaultFec(id))
|
int protection = component(id, KEY_PROTECTION, defaultProtection(id, settings));
|
||||||
+ component(id, KEY_PROTECTION, defaultProtection(id, settings))
|
int override = component(id, KEY_OVERRIDE, 0);
|
||||||
+ component(id, KEY_OVERRIDE, 0)
|
int bitrate = component(id, KEY_BITRATE, defaultBitrate(id));
|
||||||
+ 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) {
|
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. */
|
/** Best MIME for AUTO: lowest score among codecs both sides can use. */
|
||||||
|
@Nullable
|
||||||
public static String negotiateBestVideoMime(List<String> senderEncoders, List<String> receiverDecoders,
|
public static String negotiateBestVideoMime(List<String> senderEncoders, List<String> receiverDecoders,
|
||||||
CastSettings settings) {
|
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);
|
List<String> common = commonMimes(senderEncoders, receiverDecoders);
|
||||||
if (common.isEmpty()) {
|
if (common.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String best = null;
|
List<NegotiationCandidate> candidates = new ArrayList<>();
|
||||||
int bestScore = Integer.MAX_VALUE;
|
|
||||||
for (String mime : common) {
|
for (String mime : common) {
|
||||||
int score = Math.max(
|
candidates.add(buildCandidate(mime, settings));
|
||||||
videoMimeScoreOnDevice(mime, false, settings),
|
|
||||||
assumedRemoteEncoderScore(mime, settings));
|
|
||||||
if (score < bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
best = mime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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. */
|
/** 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) {
|
private static List<String> commonMimes(List<String> enc, List<String> dec) {
|
||||||
List<String> out = new ArrayList<>();
|
List<String> out = new ArrayList<>();
|
||||||
if (enc == null || dec == null) {
|
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) {
|
private static JSONObject loadJson(Context context) {
|
||||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(
|
||||||
context.getAssets().open(ASSET), StandardCharsets.UTF_8))) {
|
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.CastSettings;
|
||||||
import com.foxx.androidcast.media.CodecNegotiator;
|
import com.foxx.androidcast.media.CodecNegotiator;
|
||||||
|
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||||
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
|
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
|
||||||
import com.foxx.androidcast.sender.CodecCatalog;
|
import com.foxx.androidcast.sender.CodecCatalog;
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ public class CastSession {
|
|||||||
throw new IOException("Codec negotiation timeout");
|
throw new IOException("Codec negotiation timeout");
|
||||||
}
|
}
|
||||||
settings.setNegotiatedVideoMime(negotiated);
|
settings.setNegotiatedVideoMime(negotiated);
|
||||||
|
CodecPriorityCatalog.logReceivedCodec("sender", negotiated, settings);
|
||||||
return new HandshakeResult(deviceName, settings, negotiated);
|
return new HandshakeResult(deviceName, settings, negotiated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ public class CastSession {
|
|||||||
List<String> receiverCaps = CodecCatalog.localDecoderMimes();
|
List<String> receiverCaps = CodecCatalog.localDecoderMimes();
|
||||||
transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps));
|
transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps));
|
||||||
String mime = CodecNegotiator.negotiate(senderCaps, 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));
|
transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime));
|
||||||
remote.setNegotiatedVideoMime(mime);
|
remote.setNegotiatedVideoMime(mime);
|
||||||
return new HandshakeResult(senderName, remote, mime);
|
return new HandshakeResult(senderName, remote, mime);
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ public final class CodecCatalog {
|
|||||||
choices.addAll(rest);
|
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() {
|
private static void ensureCapabilities() {
|
||||||
if (capabilities != null) {
|
if (capabilities != null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ public final class MultiCastCoordinator {
|
|||||||
CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
|
CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
|
||||||
applyNegotiatedProtection(session, peerSettings, hs.settings);
|
applyNegotiatedProtection(session, peerSettings, hs.settings);
|
||||||
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
|
||||||
|
+ " score="
|
||||||
|
+ com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime(
|
||||||
|
hs.negotiatedVideoMime, hs.settings));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
last = e;
|
last = e;
|
||||||
Log.w(TAG, "Peer failed " + t.name + ": " + e.getMessage());
|
Log.w(TAG, "Peer failed " + t.name + ": " + e.getMessage());
|
||||||
|
|||||||
@@ -25,6 +25,13 @@
|
|||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="@string/dev_show_receiver_debug_overlay" />
|
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
|
<CheckBox
|
||||||
android:id="@+id/check_grab_session_stats"
|
android:id="@+id/check_grab_session_stats"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -32,5 +39,19 @@
|
|||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="@string/dev_grab_session_stats" />
|
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>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -187,7 +187,11 @@
|
|||||||
<string name="licenses_load_error">Не могу найти факл лицензий</string>
|
<string name="licenses_load_error">Не могу найти факл лицензий</string>
|
||||||
<string name="developer_settings_title">Для разработчиков</string>
|
<string name="developer_settings_title">Для разработчиков</string>
|
||||||
<string name="dev_show_receiver_debug_overlay">Показывать отладочный оверлей на приёмнике</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_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="label_audio_codec">Кодек для звука</string>
|
||||||
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
||||||
<string name="codec_libvpx_vp9">libvpx (VP9)</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="licenses_load_error">Could not load license text.</string>
|
||||||
<string name="developer_settings_title">Developer settings</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_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_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="label_audio_codec">Voice codec</string>
|
||||||
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
|
||||||
<string name="codec_libvpx_vp9">libvpx (VP9)</string>
|
<string name="codec_libvpx_vp9">libvpx (VP9)</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user