mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
snap
This commit is contained in:
34
app/src/main/assets/codecs.json
Normal file
34
app/src/main/assets/codecs.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Optional overrides for codec priority (lower total score = higher priority). Tier bases: HW=0, SW=500, passthrough/raw=1000.",
|
||||||
|
"video": {
|
||||||
|
"h264": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"h265": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"vp8": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"vp9": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"passthrough": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"aac": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"opus": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"speex": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
},
|
||||||
|
"pcm": {
|
||||||
|
"priority_override_score": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package com.foxx.androidcast;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||||
|
|
||||||
/** Applies saved theme and locale at process start. */
|
/** Applies saved theme and locale at process start. */
|
||||||
public class AndroidCastApplication extends Application {
|
public class AndroidCastApplication extends Application {
|
||||||
@Override
|
@Override
|
||||||
@@ -13,6 +15,7 @@ public class AndroidCastApplication extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
CodecPriorityCatalog.init(this);
|
||||||
CastLocaleHelper.applyStoredLocale(this);
|
CastLocaleHelper.applyStoredLocale(this);
|
||||||
CastThemeHelper.applyNightMode(this);
|
CastThemeHelper.applyNightMode(this);
|
||||||
CastTrayNotifier.syncOnAppStart(this);
|
CastTrayNotifier.syncOnAppStart(this);
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ public final class CastSettingsBinder {
|
|||||||
|
|
||||||
private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
Spinner spinner = activity.findViewById(spinnerId);
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
List<CodecCatalog.VideoCodecChoice> choices = CodecCatalog.videoCodecChoicesForUi();
|
List<CodecCatalog.VideoCodecChoice> choices = CodecCatalog.videoCodecChoicesForUi(settings);
|
||||||
CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()];
|
CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()];
|
||||||
String[] labels = new String[choices.size()];
|
String[] labels = new String[choices.size()];
|
||||||
boolean[] selectable = new boolean[choices.size()];
|
boolean[] selectable = new boolean[choices.size()];
|
||||||
@@ -215,7 +215,7 @@ public final class CastSettingsBinder {
|
|||||||
if (spinner == null) {
|
if (spinner == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi();
|
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi(settings);
|
||||||
CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()];
|
CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()];
|
||||||
String[] labels = new String[choices.size()];
|
String[] labels = new String[choices.size()];
|
||||||
boolean[] selectable = new boolean[choices.size()];
|
boolean[] selectable = new boolean[choices.size()];
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.foxx.androidcast.media;
|
|||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.sender.CodecCatalog;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -12,19 +12,19 @@ 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[] PREFERENCE_ORDER = {
|
|
||||||
MediaFormat.MIMETYPE_VIDEO_HEVC,
|
|
||||||
MediaFormat.MIMETYPE_VIDEO_AVC,
|
|
||||||
MediaFormat.MIMETYPE_VIDEO_VP9,
|
|
||||||
MediaFormat.MIMETYPE_VIDEO_VP8,
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||||
|
CastSettings.VideoCodec preference, CastSettings negotiationSettings) {
|
||||||
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();
|
||||||
|
|
||||||
if (preference == CastSettings.VideoCodec.H264) {
|
if (preference == CastSettings.VideoCodec.H264) {
|
||||||
return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
|
return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
|
||||||
}
|
}
|
||||||
@@ -37,10 +37,11 @@ public final class CodecNegotiator {
|
|||||||
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
||||||
return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
|
return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
|
||||||
}
|
}
|
||||||
for (String mime : PREFERENCE_ORDER) {
|
|
||||||
if (enc.contains(mime) && dec.contains(mime)) {
|
String best = CodecPriorityCatalog.negotiateBestVideoMime(
|
||||||
return mime;
|
senderEncoders, receiverDecoders, scoreSettings);
|
||||||
}
|
if (best != null) {
|
||||||
|
return best;
|
||||||
}
|
}
|
||||||
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;
|
return MediaFormat.MIMETYPE_VIDEO_AVC;
|
||||||
@@ -87,4 +88,9 @@ public final class CodecNegotiator {
|
|||||||
return MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)
|
return MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)
|
||||||
|| MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime);
|
|| MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Score for diagnostics (lower = preferred). */
|
||||||
|
public static int priorityScoreForNegotiatedMime(String mime, CastSettings settings) {
|
||||||
|
return CodecPriorityCatalog.combinedVideoMimeScore(mime, settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
package com.foxx.androidcast.media;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||||
|
import com.foxx.androidcast.sender.CodecCatalog;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
|
||||||
|
* Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
|
||||||
|
*/
|
||||||
|
public final class CodecPriorityCatalog {
|
||||||
|
private static final String TAG = "CodecPriority";
|
||||||
|
private static final String ASSET = "codecs.json";
|
||||||
|
|
||||||
|
private static final String KEY_INTERNAL = "internal_codec_score";
|
||||||
|
private static final String KEY_FEC = "no_inband_fec_score";
|
||||||
|
private static final String KEY_PROTECTION = "no_inband_protection_score";
|
||||||
|
private static final String KEY_OVERRIDE = "priority_override_score";
|
||||||
|
private static final String KEY_BITRATE = "bitrate_score";
|
||||||
|
|
||||||
|
private static volatile JSONObject rootJson = new JSONObject();
|
||||||
|
private static volatile Context appContext;
|
||||||
|
|
||||||
|
private CodecPriorityCatalog() {}
|
||||||
|
|
||||||
|
public static void init(Context context) {
|
||||||
|
appContext = context.getApplicationContext();
|
||||||
|
rootJson = loadJson(appContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int priorityScore(CodecPriorityId id, CodecTier tier, CastSettings settings) {
|
||||||
|
if (id == null || tier == null) {
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int priorityScoreForVideoMime(String mime, CodecTier tier, CastSettings settings) {
|
||||||
|
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
|
||||||
|
if (id == null) {
|
||||||
|
return CodecTier.PASSTHROUGH.baseScore + 100;
|
||||||
|
}
|
||||||
|
return priorityScore(id, tier, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int priorityScore(CastSettings.VideoCodec codec, CastSettings settings) {
|
||||||
|
CodecPriorityId id = CodecPriorityId.fromVideoPreference(codec);
|
||||||
|
if (id == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
CodecTier tier = tierForVideoPreference(codec, id);
|
||||||
|
return priorityScore(id, tier, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int priorityScore(CastSettings.AudioCodec codec, CastSettings settings) {
|
||||||
|
CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec);
|
||||||
|
if (id == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
CodecTier tier = tierForAudioPreference(codec);
|
||||||
|
return priorityScore(id, tier, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Best MIME for AUTO: lowest score among codecs both sides can use. */
|
||||||
|
public static String negotiateBestVideoMime(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;
|
||||||
|
for (String mime : common) {
|
||||||
|
int score = Math.max(
|
||||||
|
videoMimeScoreOnDevice(mime, false, settings),
|
||||||
|
assumedRemoteEncoderScore(mime, settings));
|
||||||
|
if (score < bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Conservative score when sender and receiver may use different tiers for the same MIME. */
|
||||||
|
public static int combinedVideoMimeScore(String mime, CastSettings settings) {
|
||||||
|
int localEncoder = videoMimeScoreOnDevice(mime, true, settings);
|
||||||
|
int localDecoder = videoMimeScoreOnDevice(mime, false, settings);
|
||||||
|
int assumedPeerEncoder = assumedRemoteEncoderScore(mime, settings);
|
||||||
|
return Math.max(Math.max(localEncoder, localDecoder), assumedPeerEncoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates encoder-side score for the remote peer during handshake (only MIME caps known).
|
||||||
|
*/
|
||||||
|
public static int assumedRemoteEncoderScore(String mime, CastSettings remoteSettings) {
|
||||||
|
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
|
||||||
|
if (id == null) {
|
||||||
|
return CodecTier.PASSTHROUGH.baseScore + 100;
|
||||||
|
}
|
||||||
|
CodecTier tier = assumedEncoderTier(id, remoteSettings);
|
||||||
|
return priorityScore(id, tier, remoteSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CodecTier assumedEncoderTier(CodecPriorityId id, CastSettings remote) {
|
||||||
|
CastSettings.VideoCodec pref = remote != null ? remote.getVideoCodec() : CastSettings.VideoCodec.AUTO;
|
||||||
|
if (pref == CastSettings.VideoCodec.PASSTHROUGH) {
|
||||||
|
return CodecTier.PASSTHROUGH;
|
||||||
|
}
|
||||||
|
if (pref == CastSettings.VideoCodec.LIBVPX_VP8 && id == CodecPriorityId.VP8) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
if (pref == CastSettings.VideoCodec.LIBVPX_VP9 && id == CodecPriorityId.VP9) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
|
||||||
|
return CodecTier.HARDWARE;
|
||||||
|
}
|
||||||
|
return CodecTier.HARDWARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int videoMimeScoreOnDevice(String mime, boolean encoder, CastSettings settings) {
|
||||||
|
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
|
||||||
|
if (id == null) {
|
||||||
|
return CodecTier.PASSTHROUGH.baseScore + 100;
|
||||||
|
}
|
||||||
|
CodecTier tier = CodecCatalog.effectiveTierForVideoMime(mime, encoder);
|
||||||
|
return priorityScore(id, tier, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CodecTier tierForVideoPreference(CastSettings.VideoCodec preference, CodecPriorityId id) {
|
||||||
|
if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
|
||||||
|
return CodecTier.PASSTHROUGH;
|
||||||
|
}
|
||||||
|
if (preference == CastSettings.VideoCodec.LIBVPX_VP8
|
||||||
|
|| preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
return CodecTier.HARDWARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) {
|
||||||
|
if (preference == CastSettings.AudioCodec.SPEEX) {
|
||||||
|
return CodecTier.PASSTHROUGH;
|
||||||
|
}
|
||||||
|
if (preference == CastSettings.AudioCodec.OPUS) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
return CodecTier.HARDWARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> List<T> sortByPriority(List<ScoredItem<T>> items) {
|
||||||
|
List<ScoredItem<T>> copy = new ArrayList<>(items);
|
||||||
|
Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore));
|
||||||
|
List<T> out = new ArrayList<>(copy.size());
|
||||||
|
for (ScoredItem<T> item : copy) {
|
||||||
|
out.add(item.value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ScoredItem<T> {
|
||||||
|
public final T value;
|
||||||
|
public final int priorityScore;
|
||||||
|
|
||||||
|
public ScoredItem(T value, int priorityScore) {
|
||||||
|
this.value = value;
|
||||||
|
this.priorityScore = priorityScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> commonMimes(List<String> enc, List<String> dec) {
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
if (enc == null || dec == null) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (String mime : enc) {
|
||||||
|
if (mime != null && containsIgnoreCase(dec, mime)) {
|
||||||
|
out.add(mime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsIgnoreCase(List<String> list, String mime) {
|
||||||
|
for (String s : list) {
|
||||||
|
if (s != null && s.equalsIgnoreCase(mime)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int component(CodecPriorityId id, String key, int compileDefault) {
|
||||||
|
try {
|
||||||
|
JSONObject section = sectionFor(id);
|
||||||
|
if (section == null) {
|
||||||
|
return compileDefault;
|
||||||
|
}
|
||||||
|
JSONObject codec = section.optJSONObject(id.jsonKey);
|
||||||
|
if (codec != null && codec.has(key)) {
|
||||||
|
return codec.optInt(key, compileDefault);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return compileDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject sectionFor(CodecPriorityId id) {
|
||||||
|
if (rootJson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (id) {
|
||||||
|
case H264:
|
||||||
|
case H265:
|
||||||
|
case VP8:
|
||||||
|
case VP9:
|
||||||
|
case PASSTHROUGH:
|
||||||
|
return rootJson.optJSONObject("video");
|
||||||
|
case AAC:
|
||||||
|
case OPUS:
|
||||||
|
case SPEEX:
|
||||||
|
case PCM:
|
||||||
|
default:
|
||||||
|
return rootJson.optJSONObject("audio");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int defaultInternal(CodecPriorityId id) {
|
||||||
|
switch (id) {
|
||||||
|
case H264:
|
||||||
|
case VP9:
|
||||||
|
return 0;
|
||||||
|
case H265:
|
||||||
|
case OPUS:
|
||||||
|
return 1;
|
||||||
|
case VP8:
|
||||||
|
case AAC:
|
||||||
|
return 2;
|
||||||
|
case SPEEX:
|
||||||
|
return 5;
|
||||||
|
case PCM:
|
||||||
|
case PASSTHROUGH:
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int defaultFec(CodecPriorityId id) {
|
||||||
|
switch (id) {
|
||||||
|
case H264:
|
||||||
|
case OPUS:
|
||||||
|
return 0;
|
||||||
|
case H265:
|
||||||
|
case VP8:
|
||||||
|
case AAC:
|
||||||
|
return 1;
|
||||||
|
case VP9:
|
||||||
|
return 2;
|
||||||
|
case SPEEX:
|
||||||
|
return 5;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int defaultProtection(CodecPriorityId id, CastSettings settings) {
|
||||||
|
switch (id) {
|
||||||
|
case VP8:
|
||||||
|
case VP9:
|
||||||
|
return vpxInbandProtectionScore(settings);
|
||||||
|
case H264:
|
||||||
|
case H265:
|
||||||
|
return 1;
|
||||||
|
case OPUS:
|
||||||
|
return 0;
|
||||||
|
case SPEEX:
|
||||||
|
return 5;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 0 when libvpx in-band protection is available and enabled; 2 otherwise. */
|
||||||
|
private static int vpxInbandProtectionScore(CastSettings settings) {
|
||||||
|
if (!NativeCodecBridge.isLibvpxAvailable()) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (settings == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
CastSettings.StreamProtection mode = settings.getStreamProtection();
|
||||||
|
if (mode == null || mode == CastSettings.StreamProtection.AUTO
|
||||||
|
|| mode == CastSettings.StreamProtection.NONE) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int defaultBitrate(CodecPriorityId id) {
|
||||||
|
switch (id) {
|
||||||
|
case H265:
|
||||||
|
case VP9:
|
||||||
|
case OPUS:
|
||||||
|
return 0;
|
||||||
|
case H264:
|
||||||
|
case AAC:
|
||||||
|
return 1;
|
||||||
|
case VP8:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject loadJson(Context context) {
|
||||||
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(
|
||||||
|
context.getAssets().open(ASSET), StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = r.readLine()) != null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
return new JSONObject(sb.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Using compile-time codec scores only (" + e.getMessage() + ")");
|
||||||
|
return new JSONObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.foxx.androidcast.media;
|
||||||
|
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
|
||||||
|
/** Canonical codec id for scoring tables and {@code codecs.json} keys. */
|
||||||
|
public enum CodecPriorityId {
|
||||||
|
H264("h264"),
|
||||||
|
H265("h265"),
|
||||||
|
VP8("vp8"),
|
||||||
|
VP9("vp9"),
|
||||||
|
PASSTHROUGH("passthrough"),
|
||||||
|
AAC("aac"),
|
||||||
|
OPUS("opus"),
|
||||||
|
SPEEX("speex"),
|
||||||
|
PCM("pcm");
|
||||||
|
|
||||||
|
public final String jsonKey;
|
||||||
|
|
||||||
|
CodecPriorityId(String jsonKey) {
|
||||||
|
this.jsonKey = jsonKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CodecPriorityId fromVideoMime(String mime) {
|
||||||
|
if (mime == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (MediaFormat.MIMETYPE_VIDEO_AVC.equalsIgnoreCase(mime)) {
|
||||||
|
return H264;
|
||||||
|
}
|
||||||
|
if (MediaFormat.MIMETYPE_VIDEO_HEVC.equalsIgnoreCase(mime)) {
|
||||||
|
return H265;
|
||||||
|
}
|
||||||
|
if (MediaFormat.MIMETYPE_VIDEO_VP8.equalsIgnoreCase(mime)) {
|
||||||
|
return VP8;
|
||||||
|
}
|
||||||
|
if (MediaFormat.MIMETYPE_VIDEO_VP9.equalsIgnoreCase(mime)) {
|
||||||
|
return VP9;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CodecPriorityId fromVideoPreference(CastSettings.VideoCodec codec) {
|
||||||
|
if (codec == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (codec) {
|
||||||
|
case H264:
|
||||||
|
return H264;
|
||||||
|
case H265:
|
||||||
|
return H265;
|
||||||
|
case LIBVPX_VP8:
|
||||||
|
return VP8;
|
||||||
|
case LIBVPX_VP9:
|
||||||
|
return VP9;
|
||||||
|
case PASSTHROUGH:
|
||||||
|
return PASSTHROUGH;
|
||||||
|
case AUTO:
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CodecPriorityId fromAudioPreference(CastSettings.AudioCodec codec) {
|
||||||
|
if (codec == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (codec) {
|
||||||
|
case AAC:
|
||||||
|
return AAC;
|
||||||
|
case OPUS:
|
||||||
|
return OPUS;
|
||||||
|
case SPEEX:
|
||||||
|
return SPEEX;
|
||||||
|
case AUTO:
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toMime() {
|
||||||
|
switch (this) {
|
||||||
|
case H264:
|
||||||
|
return MediaFormat.MIMETYPE_VIDEO_AVC;
|
||||||
|
case H265:
|
||||||
|
return MediaFormat.MIMETYPE_VIDEO_HEVC;
|
||||||
|
case VP8:
|
||||||
|
return MediaFormat.MIMETYPE_VIDEO_VP8;
|
||||||
|
case VP9:
|
||||||
|
return MediaFormat.MIMETYPE_VIDEO_VP9;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/src/main/java/com/foxx/androidcast/media/CodecTier.java
Normal file
14
app/src/main/java/com/foxx/androidcast/media/CodecTier.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.foxx.androidcast.media;
|
||||||
|
|
||||||
|
/** Base tier added before per-codec component scores (lower total = higher priority). */
|
||||||
|
public enum CodecTier {
|
||||||
|
HARDWARE(0),
|
||||||
|
SOFTWARE(500),
|
||||||
|
PASSTHROUGH(1000);
|
||||||
|
|
||||||
|
public final int baseScore;
|
||||||
|
|
||||||
|
CodecTier(int baseScore) {
|
||||||
|
this.baseScore = baseScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,7 +118,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()));
|
PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote);
|
||||||
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);
|
||||||
|
|||||||
@@ -6,61 +6,87 @@ import android.media.MediaFormat;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||||
|
import com.foxx.androidcast.media.CodecTier;
|
||||||
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/** Discovers device video encoder/decoder MIME types. */
|
/** Discovers device video encoder/decoder MIME types and UI codec choices with priority scores. */
|
||||||
public final class CodecCatalog {
|
public final class CodecCatalog {
|
||||||
|
private static volatile CapabilitySnapshot capabilities;
|
||||||
|
|
||||||
private CodecCatalog() {}
|
private CodecCatalog() {}
|
||||||
|
|
||||||
public static final class VideoCodecChoice {
|
public static final class VideoCodecChoice {
|
||||||
public final CastSettings.VideoCodec codec;
|
public final CastSettings.VideoCodec codec;
|
||||||
public final boolean selectable;
|
public final boolean selectable;
|
||||||
|
/** Lower = higher priority in UI and AUTO negotiation hints. */
|
||||||
|
public final int priorityScore;
|
||||||
|
|
||||||
public VideoCodecChoice(CastSettings.VideoCodec codec, boolean selectable) {
|
public VideoCodecChoice(CastSettings.VideoCodec codec, boolean selectable, int priorityScore) {
|
||||||
this.codec = codec;
|
this.codec = codec;
|
||||||
this.selectable = selectable;
|
this.selectable = selectable;
|
||||||
|
this.priorityScore = priorityScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class AudioCodecChoice {
|
public static final class AudioCodecChoice {
|
||||||
public final CastSettings.AudioCodec codec;
|
public final CastSettings.AudioCodec codec;
|
||||||
public final boolean selectable;
|
public final boolean selectable;
|
||||||
|
public final int priorityScore;
|
||||||
|
|
||||||
public AudioCodecChoice(CastSettings.AudioCodec codec, boolean selectable) {
|
public AudioCodecChoice(CastSettings.AudioCodec codec, boolean selectable, int priorityScore) {
|
||||||
this.codec = codec;
|
this.codec = codec;
|
||||||
this.selectable = selectable;
|
this.selectable = selectable;
|
||||||
|
this.priorityScore = priorityScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selectable + disabled future codecs for settings spinners. */
|
/** Selectable codecs for settings spinners (AUTO first, then by ascending priority score). */
|
||||||
public static List<VideoCodecChoice> videoCodecChoicesForUi() {
|
public static List<VideoCodecChoice> videoCodecChoicesForUi(CastSettings settings) {
|
||||||
|
ensureCapabilities();
|
||||||
List<VideoCodecChoice> out = new ArrayList<>();
|
List<VideoCodecChoice> out = new ArrayList<>();
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true));
|
out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true, 0));
|
||||||
Set<String> enc = new LinkedHashSet<>(localEncoderMimes());
|
Set<String> enc = capabilities.hardwareEncoderMimes;
|
||||||
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.H264, true));
|
out.add(scoredVideo(CastSettings.VideoCodec.H264, true, settings));
|
||||||
}
|
}
|
||||||
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
|
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.H265, true));
|
out.add(scoredVideo(CastSettings.VideoCodec.H265, true, settings));
|
||||||
}
|
}
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.LIBVPX_VP8, true));
|
if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP8)) {
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.LIBVPX_VP9, true));
|
out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP8, true, settings));
|
||||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.PASSTHROUGH, true));
|
}
|
||||||
|
if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP9)) {
|
||||||
|
out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP9, true, settings));
|
||||||
|
}
|
||||||
|
out.add(scoredVideo(CastSettings.VideoCodec.PASSTHROUGH, true, settings));
|
||||||
|
sortVideoChoices(out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<VideoCodecChoice> videoCodecChoicesForUi() {
|
||||||
|
return videoCodecChoicesForUi(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<AudioCodecChoice> audioCodecChoicesForUi(CastSettings settings) {
|
||||||
|
List<AudioCodecChoice> out = new ArrayList<>();
|
||||||
|
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0));
|
||||||
|
out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings));
|
||||||
|
out.add(scoredAudio(CastSettings.AudioCodec.OPUS, true, settings));
|
||||||
|
out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, true, settings));
|
||||||
|
sortAudioChoices(out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<AudioCodecChoice> audioCodecChoicesForUi() {
|
public static List<AudioCodecChoice> audioCodecChoicesForUi() {
|
||||||
List<AudioCodecChoice> out = new ArrayList<>();
|
return audioCodecChoicesForUi(null);
|
||||||
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true));
|
|
||||||
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AAC, true));
|
|
||||||
out.add(new AudioCodecChoice(CastSettings.AudioCodec.SPEEX, true));
|
|
||||||
out.add(new AudioCodecChoice(CastSettings.AudioCodec.OPUS, true));
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<CastSettings.VideoCodec> supportedChoices() {
|
public static List<CastSettings.VideoCodec> supportedChoices() {
|
||||||
@@ -74,14 +100,115 @@ public final class CodecCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> localEncoderMimes() {
|
public static List<String> localEncoderMimes() {
|
||||||
return listCodecMimes(true);
|
ensureCapabilities();
|
||||||
|
return new ArrayList<>(capabilities.advertisedEncoderMimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> localDecoderMimes() {
|
public static List<String> localDecoderMimes() {
|
||||||
return listCodecMimes(false);
|
ensureCapabilities();
|
||||||
|
return new ArrayList<>(capabilities.advertisedDecoderMimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> listCodecMimes(boolean encoder) {
|
public static boolean hasHardwareCodec(String mime, boolean encoder) {
|
||||||
|
ensureCapabilities();
|
||||||
|
Set<String> set = encoder ? capabilities.hardwareEncoderMimes : capabilities.hardwareDecoderMimes;
|
||||||
|
return containsIgnoreCase(set, mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasSoftwareCodec(String mime, boolean encoder) {
|
||||||
|
ensureCapabilities();
|
||||||
|
Set<String> set = encoder ? capabilities.softwareEncoderMimes : capabilities.softwareDecoderMimes;
|
||||||
|
return containsIgnoreCase(set, mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tier used for scoring on this device for a MIME (HW preferred over SW when both exist). */
|
||||||
|
public static CodecTier effectiveTierForVideoMime(String mime, boolean encoder) {
|
||||||
|
if (hasHardwareCodec(mime, encoder)) {
|
||||||
|
return CodecTier.HARDWARE;
|
||||||
|
}
|
||||||
|
if (hasSoftwareCodec(mime, encoder)) {
|
||||||
|
return CodecTier.SOFTWARE;
|
||||||
|
}
|
||||||
|
return CodecTier.PASSTHROUGH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canUseVideoMimeOnDevice(String mime, boolean encoder) {
|
||||||
|
return hasHardwareCodec(mime, encoder) || hasSoftwareCodec(mime, encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VideoCodecChoice scoredVideo(CastSettings.VideoCodec codec, boolean selectable,
|
||||||
|
CastSettings settings) {
|
||||||
|
int score = CodecPriorityCatalog.priorityScore(codec, settings);
|
||||||
|
return new VideoCodecChoice(codec, selectable, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AudioCodecChoice scoredAudio(CastSettings.AudioCodec codec, boolean selectable,
|
||||||
|
CastSettings settings) {
|
||||||
|
int score = CodecPriorityCatalog.priorityScore(codec, settings);
|
||||||
|
return new AudioCodecChoice(codec, selectable, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sortVideoChoices(List<VideoCodecChoice> choices) {
|
||||||
|
if (choices.size() <= 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VideoCodecChoice auto = choices.get(0);
|
||||||
|
List<VideoCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
|
||||||
|
Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
|
||||||
|
choices.clear();
|
||||||
|
choices.add(auto);
|
||||||
|
choices.addAll(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sortAudioChoices(List<AudioCodecChoice> choices) {
|
||||||
|
if (choices.size() <= 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AudioCodecChoice auto = choices.get(0);
|
||||||
|
List<AudioCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
|
||||||
|
Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
|
||||||
|
choices.clear();
|
||||||
|
choices.add(auto);
|
||||||
|
choices.addAll(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureCapabilities() {
|
||||||
|
if (capabilities != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (CodecCatalog.class) {
|
||||||
|
if (capabilities != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capabilities = probeCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CapabilitySnapshot probeCapabilities() {
|
||||||
|
Set<String> hwEnc = probeHardwareMimes(true);
|
||||||
|
Set<String> hwDec = probeHardwareMimes(false);
|
||||||
|
Set<String> swEnc = new LinkedHashSet<>();
|
||||||
|
Set<String> swDec = new LinkedHashSet<>();
|
||||||
|
if (NativeCodecBridge.isLibvpxAvailable()) {
|
||||||
|
swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP8);
|
||||||
|
swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
||||||
|
swDec.add(MediaFormat.MIMETYPE_VIDEO_VP8);
|
||||||
|
swDec.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
||||||
|
}
|
||||||
|
Set<String> advertisedEnc = new LinkedHashSet<>(hwEnc);
|
||||||
|
advertisedEnc.addAll(swEnc);
|
||||||
|
if (advertisedEnc.isEmpty()) {
|
||||||
|
advertisedEnc.add(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
|
}
|
||||||
|
Set<String> advertisedDec = new LinkedHashSet<>(hwDec);
|
||||||
|
advertisedDec.addAll(swDec);
|
||||||
|
if (advertisedDec.isEmpty()) {
|
||||||
|
advertisedDec.add(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
|
}
|
||||||
|
return new CapabilitySnapshot(hwEnc, hwDec, swEnc, swDec, advertisedEnc, advertisedDec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> probeHardwareMimes(boolean encoder) {
|
||||||
Set<String> out = new LinkedHashSet<>();
|
Set<String> out = new LinkedHashSet<>();
|
||||||
String[] want = {
|
String[] want = {
|
||||||
MediaFormat.MIMETYPE_VIDEO_AVC,
|
MediaFormat.MIMETYPE_VIDEO_AVC,
|
||||||
@@ -119,13 +246,38 @@ public final class CodecCatalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out.isEmpty()) {
|
return out;
|
||||||
out.add(MediaFormat.MIMETYPE_VIDEO_AVC);
|
|
||||||
}
|
}
|
||||||
if (NativeCodecBridge.isLibvpxAvailable()) {
|
|
||||||
out.add(MediaFormat.MIMETYPE_VIDEO_VP8);
|
private static boolean containsIgnoreCase(Set<String> set, String mime) {
|
||||||
out.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
if (mime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String s : set) {
|
||||||
|
if (s != null && s.equalsIgnoreCase(mime)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CapabilitySnapshot {
|
||||||
|
final Set<String> hardwareEncoderMimes;
|
||||||
|
final Set<String> hardwareDecoderMimes;
|
||||||
|
final Set<String> softwareEncoderMimes;
|
||||||
|
final Set<String> softwareDecoderMimes;
|
||||||
|
final Set<String> advertisedEncoderMimes;
|
||||||
|
final Set<String> advertisedDecoderMimes;
|
||||||
|
|
||||||
|
CapabilitySnapshot(Set<String> hardwareEncoderMimes, Set<String> hardwareDecoderMimes,
|
||||||
|
Set<String> softwareEncoderMimes, Set<String> softwareDecoderMimes,
|
||||||
|
Set<String> advertisedEncoderMimes, Set<String> advertisedDecoderMimes) {
|
||||||
|
this.hardwareEncoderMimes = hardwareEncoderMimes;
|
||||||
|
this.hardwareDecoderMimes = hardwareDecoderMimes;
|
||||||
|
this.softwareEncoderMimes = softwareEncoderMimes;
|
||||||
|
this.softwareDecoderMimes = softwareDecoderMimes;
|
||||||
|
this.advertisedEncoderMimes = advertisedEncoderMimes;
|
||||||
|
this.advertisedDecoderMimes = advertisedDecoderMimes;
|
||||||
}
|
}
|
||||||
return new ArrayList<>(out);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user