mirror of
https://github.com/androidx/media.git
synced 2025-05-15 19:49:50 +08:00
Add additional Widevine samples + improve errors.
* Add additional Widevine samples. * Improve error messaging in demo app around decoders. * Display toasts for playback errors related to missing insecure decoders, missing secure decoders, decoder instantiation failure and decoder query failure. * Remove checks from SampleChooserActivity, since the above largely covers off this problem.
This commit is contained in:
parent
c2df814b58
commit
a6bfe02d24
@ -16,7 +16,10 @@
|
||||
package com.google.android.exoplayer.demo;
|
||||
|
||||
import com.google.android.exoplayer.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
|
||||
@ -99,9 +102,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
private static final int MENU_GROUP_TRACKS = 1;
|
||||
private static final int ID_OFFSET = 2;
|
||||
|
||||
// For use when requesting permission.
|
||||
private static final String URI_FILE_SCHEME = "file";
|
||||
|
||||
private static final CookieManager defaultCookieManager;
|
||||
static {
|
||||
defaultCookieManager = new CookieManager();
|
||||
@ -394,13 +394,35 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
String errorString = null;
|
||||
if (e instanceof UnsupportedDrmException) {
|
||||
// Special case DRM failures.
|
||||
UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e;
|
||||
int stringId = Util.SDK_INT < 18 ? R.string.drm_error_not_supported
|
||||
errorString = getString(Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
||||
: unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.drm_error_unsupported_scheme : R.string.drm_error_unknown;
|
||||
Toast.makeText(getApplicationContext(), stringId, Toast.LENGTH_LONG).show();
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
|
||||
} else if (e instanceof ExoPlaybackException
|
||||
&& e.getCause() instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) e.getCause();
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString = getString(R.string.error_no_secure_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString = getString(R.string.error_no_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString = getString(R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
}
|
||||
}
|
||||
if (errorString != null) {
|
||||
Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
playerNeedsPrepare = true;
|
||||
updateButtonVisibilities();
|
||||
|
@ -15,17 +15,13 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.demo;
|
||||
|
||||
import com.google.android.exoplayer.MediaCodecUtil;
|
||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer.demo.Samples.Sample;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -40,8 +36,6 @@ import android.widget.TextView;
|
||||
*/
|
||||
public class SampleChooserActivity extends Activity {
|
||||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -52,27 +46,20 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
sampleAdapter.add(new Header("YouTube DASH"));
|
||||
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
|
||||
try {
|
||||
if (MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_VP9, false) != null) {
|
||||
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM);
|
||||
}
|
||||
} catch (DecoderQueryException e) {
|
||||
Log.e(TAG, "Failed to query vp9 decoder", e);
|
||||
}
|
||||
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_WEBM);
|
||||
sampleAdapter.add(new Header("Widevine DASH Policy Tests (GTS)"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
|
||||
sampleAdapter.add(new Header("Widevine DASH"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_DASH_MP4);
|
||||
try {
|
||||
if (MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_VP9, false) != null) {
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_VP9_WEBM_CLEAR);
|
||||
}
|
||||
if (MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_VP9, true) != null) {
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_VP9_WEBM_SECURE);
|
||||
}
|
||||
} catch (DecoderQueryException e) {
|
||||
Log.e(TAG, "Failed to query vp9 decoder", e);
|
||||
}
|
||||
sampleAdapter.add(new Header("Widevine HDCP Capabilities Tests"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_HDCP);
|
||||
sampleAdapter.add(new Header("Widevine DASH: MP4,H264"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_H264_MP4_CLEAR);
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_H264_MP4_SECURE);
|
||||
sampleAdapter.add(new Header("Widevine DASH: WebM,VP9"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_VP9_WEBM_CLEAR);
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_VP9_WEBM_SECURE);
|
||||
sampleAdapter.add(new Header("Widevine DASH: MP4,H265"));
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_H265_MP4_CLEAR);
|
||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_H265_MP4_SECURE);
|
||||
sampleAdapter.add(new Header("SmoothStreaming"));
|
||||
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
|
||||
sampleAdapter.add(new Header("HLS"));
|
||||
|
@ -80,7 +80,7 @@ import java.util.Locale;
|
||||
};
|
||||
|
||||
private static final String WIDEVINE_GTS_MPD =
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears.mpd";
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd";
|
||||
public static final Sample[] WIDEVINE_GTS = new Sample[] {
|
||||
new Sample("WV: HDCP not specified", "d286538032258a1c", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
@ -88,32 +88,122 @@ import java.util.Locale;
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP required", "e06c39f1151da3df", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure video path required", "0894c7c8719b28a0", "widevine_test",
|
||||
new Sample("WV: Secure video path required (MP4,H264)", "0894c7c8719b28a0", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure video path required (WebM,VP9)", "0894c7c8719b28a0", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure video path required (MP4,H265)", "0894c7c8719b28a0", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: 30s license duration (fails at ~30s)", "f9a34cab7b05881a", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_DASH_MP4 = new Sample[] {
|
||||
new Sample("WV: Clear (MP4,H264)",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/clear/h264/tears.mpd",
|
||||
public static final Sample[] WIDEVINE_HDCP = new Sample[] {
|
||||
new Sample("WV: HDCP: None (not required)", "HDCP_None", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP: 1.0 required", "HDCP_V1", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP: 2.0 required", "HDCP_V2", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP: 2.1 required", "HDCP_V2_1", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP: 2.2 required", "HDCP_V2_2", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: HDCP: No digital output", "HDCP_NO_DIGTAL_OUTPUT", "widevine_test",
|
||||
WIDEVINE_GTS_MPD, PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_H264_MP4_CLEAR = new Sample[] {
|
||||
new Sample("WV: Clear SD & HD (MP4,H264)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure (MP4,H264)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears.mpd",
|
||||
new Sample("WV: Clear SD (MP4,H264)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear HD (MP4,H264)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear UHD (MP4,H264)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_H264_MP4_SECURE = new Sample[] {
|
||||
new Sample("WV: Secure SD & HD (MP4,H264)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure SD (MP4,H264)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure HD (MP4,H264)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure UHD (MP4,H264)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_VP9_WEBM_CLEAR = new Sample[] {
|
||||
new Sample("WV: Clear (WebM,VP9)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/vp9/sintel-multicodec-4k/sintel-vp9.mpd",
|
||||
new Sample("WV: Clear SD & HD (WebM,VP9)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear SD (WebM,VP9)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear HD (WebM,VP9)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear UHD (WebM,VP9)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_VP9_WEBM_SECURE = new Sample[] {
|
||||
new Sample("WV: Secure (WebM,VP9)", "01234567", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/sintel-multicodec-4k/sintel-vp9.mpd",
|
||||
new Sample("WV: Secure SD & HD (WebM,VP9)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure SD (WebM,VP9)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure HD (WebM,VP9)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure UHD (WebM,VP9)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_H265_MP4_CLEAR = new Sample[] {
|
||||
new Sample("WV: Clear SD & HD (MP4,H265)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear SD (MP4,H265)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear HD (MP4,H265)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Clear UHD (MP4,H265)",
|
||||
"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
public static final Sample[] WIDEVINE_H265_MP4_SECURE = new Sample[] {
|
||||
new Sample("WV: Secure SD & HD (MP4,H265)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure SD (MP4,H265)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure HD (MP4,H265)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
new Sample("WV: Secure UHD (MP4,H265)", "", "widevine_test",
|
||||
"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||
PlayerActivity.TYPE_DASH),
|
||||
};
|
||||
|
||||
|
@ -37,11 +37,19 @@
|
||||
|
||||
<string name="off">[off]</string>
|
||||
|
||||
<string name="drm_error_not_supported">Protected content not supported on API levels below 18</string>
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="drm_error_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="drm_error_unknown">An unknown DRM error occurred</string>
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
|
||||
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_querying_decoders">Unable to query device decoders</string>
|
||||
|
||||
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||
|
||||
<string name="storage_permission_denied">Permission to access storage was denied</string>
|
||||
|
||||
|
@ -81,6 +81,16 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
|
||||
private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
|
||||
private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
|
||||
|
||||
/**
|
||||
* The mime type for which a decoder was being initialized.
|
||||
*/
|
||||
public final String mimeType;
|
||||
|
||||
/**
|
||||
* Whether it was required that the decoder support a secure output path.
|
||||
*/
|
||||
public final boolean secureDecoderRequired;
|
||||
|
||||
/**
|
||||
* The name of the decoder that failed to initialize. Null if no suitable decoder was found.
|
||||
*/
|
||||
@ -91,15 +101,20 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
|
||||
*/
|
||||
public final String diagnosticInfo;
|
||||
|
||||
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, int errorCode) {
|
||||
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause,
|
||||
boolean secureDecoderRequired, int errorCode) {
|
||||
super("Decoder init failed: [" + errorCode + "], " + mediaFormat, cause);
|
||||
this.mimeType = mediaFormat.mimeType;
|
||||
this.secureDecoderRequired = secureDecoderRequired;
|
||||
this.decoderName = null;
|
||||
this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
|
||||
}
|
||||
|
||||
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause,
|
||||
String decoderName) {
|
||||
boolean secureDecoderRequired, String decoderName) {
|
||||
super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
|
||||
this.mimeType = mediaFormat.mimeType;
|
||||
this.secureDecoderRequired = secureDecoderRequired;
|
||||
this.decoderName = decoderName;
|
||||
this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
|
||||
}
|
||||
@ -313,12 +328,12 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
|
||||
decoderInfo = getDecoderInfo(mimeType, requiresSecureDecoder);
|
||||
} catch (DecoderQueryException e) {
|
||||
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e,
|
||||
DecoderInitializationException.DECODER_QUERY_ERROR));
|
||||
requiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
|
||||
}
|
||||
|
||||
if (decoderInfo == null) {
|
||||
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, null,
|
||||
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
|
||||
requiresSecureDecoder, DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
|
||||
}
|
||||
|
||||
String codecName = decoderInfo.name;
|
||||
@ -343,7 +358,8 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer
|
||||
inputBuffers = codec.getInputBuffers();
|
||||
outputBuffers = codec.getOutputBuffers();
|
||||
} catch (Exception e) {
|
||||
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, codecName));
|
||||
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e,
|
||||
requiresSecureDecoder, codecName));
|
||||
}
|
||||
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
|
||||
SystemClock.elapsedRealtime() : -1;
|
||||
|
Loading…
x
Reference in New Issue
Block a user