Merge pull request #3961 from ByteHamster/show-buffering-indicator
Show buffering indicator on ExoPlayer
This commit is contained in:
commit
ae906de06d
|
@ -4,7 +4,6 @@ import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
@ -14,55 +13,51 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||||
|
import de.danoeh.antennapod.core.util.playback.IPlayer;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import org.antennapod.audio.MediaPlayer;
|
import org.antennapod.audio.MediaPlayer;
|
||||||
import de.danoeh.antennapod.core.util.playback.IPlayer;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class ExoPlayerWrapper implements IPlayer {
|
public class ExoPlayerWrapper implements IPlayer {
|
||||||
private static final String TAG = "ExoPlayerWrapper";
|
private static final String TAG = "ExoPlayerWrapper";
|
||||||
public static final int ERROR_CODE_OFFSET = 1000;
|
public static final int ERROR_CODE_OFFSET = 1000;
|
||||||
private final Context mContext;
|
private final Context context;
|
||||||
private final Disposable bufferingUpdateDisposable;
|
private final Disposable bufferingUpdateDisposable;
|
||||||
private SimpleExoPlayer mExoPlayer;
|
private SimpleExoPlayer exoPlayer;
|
||||||
private MediaSource mediaSource;
|
private MediaSource mediaSource;
|
||||||
private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener;
|
private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener;
|
||||||
private MediaPlayer.OnCompletionListener audioCompletionListener;
|
private MediaPlayer.OnCompletionListener audioCompletionListener;
|
||||||
private MediaPlayer.OnErrorListener audioErrorListener;
|
private MediaPlayer.OnErrorListener audioErrorListener;
|
||||||
private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener;
|
private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener;
|
||||||
private PlaybackParameters playbackParameters;
|
private PlaybackParameters playbackParameters;
|
||||||
|
private MediaPlayer.OnInfoListener infoListener;
|
||||||
|
|
||||||
|
|
||||||
ExoPlayerWrapper(Context context) {
|
ExoPlayerWrapper(Context context) {
|
||||||
mContext = context;
|
this.context = context;
|
||||||
mExoPlayer = createPlayer();
|
exoPlayer = createPlayer();
|
||||||
playbackParameters = mExoPlayer.getPlaybackParameters();
|
playbackParameters = exoPlayer.getPlaybackParameters();
|
||||||
|
|
||||||
bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS)
|
bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(tickNumber -> {
|
.subscribe(tickNumber -> {
|
||||||
if (bufferingUpdateListener != null) {
|
if (bufferingUpdateListener != null) {
|
||||||
bufferingUpdateListener.onBufferingUpdate(null, mExoPlayer.getBufferedPercentage());
|
bufferingUpdateListener.onBufferingUpdate(null, exoPlayer.getBufferedPercentage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private SimpleExoPlayer createPlayer() {
|
private SimpleExoPlayer createPlayer() {
|
||||||
|
@ -71,42 +66,21 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||||
loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true);
|
loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true);
|
||||||
SimpleExoPlayer p = ExoPlayerFactory.newSimpleInstance(mContext, new DefaultRenderersFactory(mContext),
|
SimpleExoPlayer p = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context),
|
||||||
new DefaultTrackSelector(), loadControl.createDefaultLoadControl());
|
new DefaultTrackSelector(), loadControl.createDefaultLoadControl());
|
||||||
p.setSeekParameters(SeekParameters.EXACT);
|
p.setSeekParameters(SeekParameters.EXACT);
|
||||||
p.addListener(new Player.EventListener() {
|
p.addListener(new Player.EventListener() {
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadingChanged(boolean isLoading) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) {
|
||||||
audioCompletionListener.onCompletion(null);
|
audioCompletionListener.onCompletion(null);
|
||||||
|
} else if (infoListener != null && playbackState == Player.STATE_BUFFERING) {
|
||||||
|
infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
|
||||||
|
} else if (infoListener != null) {
|
||||||
|
infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRepeatModeChanged(int repeatMode) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
if (audioErrorListener != null) {
|
if (audioErrorListener != null) {
|
||||||
|
@ -114,16 +88,6 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(int reason) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSeekProcessed() {
|
public void onSeekProcessed() {
|
||||||
audioSeekCompleteListener.onSeekComplete(null);
|
audioSeekCompleteListener.onSeekComplete(null);
|
||||||
|
@ -144,7 +108,7 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCurrentPosition() {
|
public int getCurrentPosition() {
|
||||||
return (int) mExoPlayer.getCurrentPosition();
|
return (int) exoPlayer.getCurrentPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -154,32 +118,32 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDuration() {
|
public int getDuration() {
|
||||||
if (mExoPlayer.getDuration() == C.TIME_UNSET) {
|
if (exoPlayer.getDuration() == C.TIME_UNSET) {
|
||||||
return PlaybackServiceMediaPlayer.INVALID_TIME;
|
return PlaybackServiceMediaPlayer.INVALID_TIME;
|
||||||
}
|
}
|
||||||
return (int) mExoPlayer.getDuration();
|
return (int) exoPlayer.getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPlaying() {
|
public boolean isPlaying() {
|
||||||
return mExoPlayer.getPlayWhenReady();
|
return exoPlayer.getPlayWhenReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void pause() {
|
public void pause() {
|
||||||
mExoPlayer.setPlayWhenReady(false);
|
exoPlayer.setPlayWhenReady(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepare() throws IllegalStateException {
|
public void prepare() throws IllegalStateException {
|
||||||
mExoPlayer.prepare(mediaSource, false, true);
|
exoPlayer.prepare(mediaSource, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
bufferingUpdateDisposable.dispose();
|
bufferingUpdateDisposable.dispose();
|
||||||
if (mExoPlayer != null) {
|
if (exoPlayer != null) {
|
||||||
mExoPlayer.release();
|
exoPlayer.release();
|
||||||
}
|
}
|
||||||
audioSeekCompleteListener = null;
|
audioSeekCompleteListener = null;
|
||||||
audioCompletionListener = null;
|
audioCompletionListener = null;
|
||||||
|
@ -189,35 +153,35 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reset() {
|
public void reset() {
|
||||||
mExoPlayer.release();
|
exoPlayer.release();
|
||||||
mExoPlayer = createPlayer();
|
exoPlayer = createPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seekTo(int i) throws IllegalStateException {
|
public void seekTo(int i) throws IllegalStateException {
|
||||||
mExoPlayer.seekTo(i);
|
exoPlayer.seekTo(i);
|
||||||
audioSeekCompleteListener.onSeekComplete(null);
|
audioSeekCompleteListener.onSeekComplete(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAudioStreamType(int i) {
|
public void setAudioStreamType(int i) {
|
||||||
AudioAttributes a = mExoPlayer.getAudioAttributes();
|
AudioAttributes a = exoPlayer.getAudioAttributes();
|
||||||
AudioAttributes.Builder b = new AudioAttributes.Builder();
|
AudioAttributes.Builder b = new AudioAttributes.Builder();
|
||||||
b.setContentType(i);
|
b.setContentType(i);
|
||||||
b.setFlags(a.flags);
|
b.setFlags(a.flags);
|
||||||
b.setUsage(a.usage);
|
b.setUsage(a.usage);
|
||||||
mExoPlayer.setAudioAttributes(b.build());
|
exoPlayer.setAudioAttributes(b.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException {
|
public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException {
|
||||||
Log.d(TAG, "setDataSource: " + s);
|
Log.d(TAG, "setDataSource: " + s);
|
||||||
DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
|
DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
|
||||||
Util.getUserAgent(mContext, mContext.getPackageName()), null,
|
Util.getUserAgent(context, context.getPackageName()), null,
|
||||||
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
|
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
true);
|
true);
|
||||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(mContext, null, httpDataSourceFactory);
|
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory);
|
||||||
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
||||||
extractorsFactory.setConstantBitrateSeekingEnabled(true);
|
extractorsFactory.setConstantBitrateSeekingEnabled(true);
|
||||||
ProgressiveMediaSource.Factory f = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
|
ProgressiveMediaSource.Factory f = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
|
||||||
|
@ -226,13 +190,13 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDisplay(SurfaceHolder sh) {
|
public void setDisplay(SurfaceHolder sh) {
|
||||||
mExoPlayer.setVideoSurfaceHolder(sh);
|
exoPlayer.setVideoSurfaceHolder(sh);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setPlaybackParams(float speed, boolean skipSilence) {
|
public void setPlaybackParams(float speed, boolean skipSilence) {
|
||||||
playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch, skipSilence);
|
playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch, skipSilence);
|
||||||
mExoPlayer.setPlaybackParameters(playbackParameters);
|
exoPlayer.setPlaybackParameters(playbackParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -242,7 +206,7 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setVolume(float v, float v1) {
|
public void setVolume(float v, float v1) {
|
||||||
mExoPlayer.setVolume(v);
|
exoPlayer.setVolume(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -252,14 +216,14 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
mExoPlayer.setPlayWhenReady(true);
|
exoPlayer.setPlayWhenReady(true);
|
||||||
// Can't set params when paused - so always set it on start in case they changed
|
// Can't set params when paused - so always set it on start in case they changed
|
||||||
mExoPlayer.setPlaybackParameters(playbackParameters);
|
exoPlayer.setPlaybackParameters(playbackParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
mExoPlayer.stop();
|
exoPlayer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setOnCompletionListener(MediaPlayer.OnCompletionListener audioCompletionListener) {
|
void setOnCompletionListener(MediaPlayer.OnCompletionListener audioCompletionListener) {
|
||||||
|
@ -275,20 +239,24 @@ public class ExoPlayerWrapper implements IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
int getVideoWidth() {
|
int getVideoWidth() {
|
||||||
if (mExoPlayer.getVideoFormat() == null) {
|
if (exoPlayer.getVideoFormat() == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return mExoPlayer.getVideoFormat().width;
|
return exoPlayer.getVideoFormat().width;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getVideoHeight() {
|
int getVideoHeight() {
|
||||||
if (mExoPlayer.getVideoFormat() == null) {
|
if (exoPlayer.getVideoFormat() == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return mExoPlayer.getVideoFormat().height;
|
return exoPlayer.getVideoFormat().height;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener) {
|
void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener) {
|
||||||
this.bufferingUpdateListener = bufferingUpdateListener;
|
this.bufferingUpdateListener = bufferingUpdateListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnInfoListener(MediaPlayer.OnInfoListener infoListener) {
|
||||||
|
this.infoListener = infoListener;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1042,6 +1042,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||||
ap.setOnSeekCompleteListener(audioSeekCompleteListener);
|
ap.setOnSeekCompleteListener(audioSeekCompleteListener);
|
||||||
ap.setOnBufferingUpdateListener(audioBufferingUpdateListener);
|
ap.setOnBufferingUpdateListener(audioBufferingUpdateListener);
|
||||||
ap.setOnErrorListener(audioErrorListener);
|
ap.setOnErrorListener(audioErrorListener);
|
||||||
|
ap.setOnInfoListener(audioInfoListener);
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Unknown media player: " + mp);
|
Log.w(TAG, "Unknown media player: " + mp);
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,13 +291,9 @@ public class PlaybackController {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (!isConnectedToPlaybackService()) {
|
int type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
|
||||||
bindToService();
|
int code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
|
||||||
return;
|
if (code == -1 || type == -1) {
|
||||||
}
|
|
||||||
int type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
|
|
||||||
int code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
|
|
||||||
if(code == -1 || type == -1) {
|
|
||||||
Log.d(TAG, "Bad arguments. Won't handle intent");
|
Log.d(TAG, "Bad arguments. Won't handle intent");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -310,6 +306,10 @@ public class PlaybackController {
|
||||||
onBufferUpdate(progress);
|
onBufferUpdate(progress);
|
||||||
break;
|
break;
|
||||||
case PlaybackService.NOTIFICATION_TYPE_RELOAD:
|
case PlaybackService.NOTIFICATION_TYPE_RELOAD:
|
||||||
|
if (!isConnectedToPlaybackService()) {
|
||||||
|
bindToService();
|
||||||
|
return;
|
||||||
|
}
|
||||||
mediaInfoLoaded = false;
|
mediaInfoLoaded = false;
|
||||||
queryService();
|
queryService();
|
||||||
onReloadNotification(intent.getIntExtra(
|
onReloadNotification(intent.getIntExtra(
|
||||||
|
|
Loading…
Reference in New Issue