MediaPlayer пропускает вперед около 6 секунд при вращении
У меня есть MediaPlayer во фрагменте, который сохраняет свой экземпляр при изменениях конфигурации. Плеер воспроизводит видео, загруженное из моего каталога ресурсов. У меня есть сценарий, настроенный с целью воспроизведения воспроизведения приложения YouTube, при котором звук продолжает воспроизводиться во время изменений конфигурации, а дисплей отсоединяется и снова подключается к медиаплееру.
Когда я запускаю воспроизведение и поворачиваю устройство, позиция переходит вперед примерно на 6 секунд, и (обязательно) звук отключается, когда это происходит. После этого воспроизведение продолжается в обычном режиме. Я понятия не имею, что может быть причиной этого.
По запросу, вот код:
public class MainFragment extends Fragment implements SurfaceHolder.Callback, MediaController.MediaPlayerControl {
private static final String TAG = MainFragment.class.getSimpleName();
AssetFileDescriptor mVideoFd;
SurfaceView mSurfaceView;
MediaPlayer mMediaPlayer;
MediaController mMediaController;
boolean mPrepared;
boolean mShouldResumePlayback;
int mBufferingPercent;
SurfaceHolder mSurfaceHolder;
@Override
public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(activity, attrs, savedInstanceState);
final String assetFileName = "test-video.mp4";
try {
mVideoFd = activity.getAssets().openFd(assetFileName);
} catch (IOException ioe) {
Log.e(TAG, "Can't open file " + assetFileName + "!");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
// initialize the media player
mMediaPlayer = new MediaPlayer();
try {
mMediaPlayer.setDataSource(mVideoFd.getFileDescriptor(), mVideoFd.getStartOffset(), mVideoFd.getLength());
} catch (IOException ioe) {
Log.e(TAG, "Unable to read video file when setting data source.");
throw new RuntimeException("Can't read assets file!");
}
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mPrepared = true;
}
});
mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
mBufferingPercent = percent;
}
});
mMediaPlayer.prepareAsync();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_main, container, false);
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface);
mSurfaceView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMediaController.show();
}
});
mSurfaceHolder = mSurfaceView.getHolder();
if (mSurfaceHolder == null) {
throw new RuntimeException("SufraceView's holder is null");
}
mSurfaceHolder.addCallback(this);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mMediaController = new MediaController(getActivity());
mMediaController.setEnabled(false);
mMediaController.setMediaPlayer(this);
mMediaController.setAnchorView(view);
}
@Override
public void onResume() {
super.onResume();
if (mShouldResumePlayback) {
start();
} else {
mSurfaceView.post(new Runnable() {
@Override
public void run() {
mMediaController.show();
}
});
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaController.setEnabled(true);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// nothing
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mMediaPlayer.setDisplay(null);
}
@Override
public void onPause() {
if (mMediaPlayer.isPlaying() && !getActivity().isChangingConfigurations()) {
pause();
mShouldResumePlayback = true;
}
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
public void onDestroyView() {
mMediaController.setAnchorView(null);
mMediaController = null;
mMediaPlayer.setDisplay(null);
mSurfaceHolder.removeCallback(this);
mSurfaceHolder = null;
mSurfaceView = null;
super.onDestroyView();
}
@Override
public void onDestroy() {
mMediaPlayer.release();
mMediaPlayer = null;
try {
mVideoFd.close();
} catch (IOException ioe) {
Log.e(TAG, "Can't close asset file..", ioe);
}
mVideoFd = null;
super.onDestroy();
}
// MediaControler methods:
@Override
public void start() {
mMediaPlayer.start();
}
@Override
public void pause() {
mMediaPlayer.pause();
}
@Override
public int getDuration() {
return mMediaPlayer.getDuration();
}
@Override
public int getCurrentPosition() {
return mMediaPlayer.getCurrentPosition();
}
@Override
public void seekTo(int pos) {
mMediaPlayer.seekTo(pos);
}
@Override
public boolean isPlaying() {
return mMediaPlayer.isPlaying();
}
@Override
public int getBufferPercentage() {
return mBufferingPercent;
}
@Override
public boolean canPause() {
return true;
}
@Override
public boolean canSeekBackward() {
return true;
}
@Override
public boolean canSeekForward() {
return true;
}
@Override
public int getAudioSessionId() {
return mMediaPlayer.getAudioSessionId();
}
}
if
блок в onPause
метод не ударил.
Обновить:
После некоторой дополнительной отладки удаление взаимодействия с SurfaceHolder приводит к исчезновению проблемы. Другими словами, если я не установлю отображение на MediaPlayer, звук будет работать нормально во время изменения конфигурации: без паузы, без пропуска. Казалось бы, есть некоторая проблема синхронизации с настройкой дисплея на MediaPlayer, которая сбивает с толку игрока.
Кроме того, я обнаружил, что вы должны hide()
MediaController перед его удалением во время изменения конфигурации. Это улучшает стабильность, но не устраняет проблему пропуска.
Еще одно обновление:
Если вам все равно, медиа-стек Android выглядит следующим образом:
MediaPlayer.java -> android_media_MediaPlayer.cpp -> MediaPlayer.cpp -> IMediaPlayer.cpp -> MediaPlayerService.cpp -> BnMediaPlayerService.cpp -> IMediaPlayerService.cpp -> * ConclayMediaPlayer> * * * * * * * и т. д.) -> *real MediaPlayerProxy* (AwesomePlayer, NuPlayer и т. д.) -> *RealMediaPlayer* (AwesomePlayerSource, NuPlayerDecoder и т. д.) -> Кодек -> HW/SW-декодер
Изучив AwesomePlayer, кажется, что этот удивительный игрок может позволить себе сделать паузу, когда вы setSurface()
:
status_t AwesomePlayer::setNativeWindow_l(const sp<ANativeWindow> &native) {
mNativeWindow = native;
if (mVideoSource == NULL) {
return OK;
}
ALOGV("attempting to reconfigure to use new surface");
bool wasPlaying = (mFlags & PLAYING) != 0;
pause_l();
mVideoRenderer.clear();
shutdownVideoDecoder_l();
status_t err = initVideoDecoder();
if (err != OK) {
ALOGE("failed to reinstantiate video decoder after surface change.");
return err;
}
if (mLastVideoTimeUs >= 0) {
mSeeking = SEEK;
mSeekTimeUs = mLastVideoTimeUs;
modifyFlags((AT_EOS | AUDIO_AT_EOS | VIDEO_AT_EOS), CLEAR);
}
if (wasPlaying) {
play_l();
}
return OK;
}
Это показывает, что установка поверхности приведет к тому, что проигрыватель разрушит любую ранее использованную поверхность, а также видеодекодер вместе с ней. Если установка нулевой поверхности не должна приводить к остановке звука, для установки новой поверхности требуется повторная инициализация видеодекодера и поиск проигрывателем текущего местоположения в видео. По соглашению, поиск никогда не приведет вас дальше, чем вы запрашиваете, то есть, если вы пересекаете ключевой кадр при поиске, вы должны приземлиться на кадре, который вы промахнули (в отличие от следующего).
Моя гипотеза заключается в том, что Android MediaPlayer не соблюдает это соглашение и при поиске переходит к следующему ключевому кадру. Это, в сочетании с видеоисточником с разреженными ключевыми кадрами, может объяснить прыжки, которые я испытываю. Я не смотрел на реализацию AwesomePlayer поиска, хотя. Мне было сказано, что переход к следующему ключевому кадру - это то, что должно произойти, если ваш MediaPlayer разрабатывается с учетом потоковой передачи, поскольку поток может быть отброшен, как только он будет использован. Суть в том, что может быть не так уж и сложно думать, что MediaPlayer выберет прыжок вперед, а не назад.
Окончательное обновление:
Хотя я до сих пор не знаю, почему воспроизведение пропускается при подключении нового Surface
в качестве дисплея для MediaPlayer
, благодаря принятому ответу, я получил воспроизведение без проблем во время вращения.
3 ответа
Благодаря ответу natez0r мне удалось заставить работать описанную установку. Однако я использую немного другой метод. Я опишу это здесь для справки.
у меня есть такой Fragment
который я отмечаю, чтобы сохранить при изменениях конфигурации. Этот фрагмент обрабатывает как воспроизведение мультимедиа (MediaPlayer
) и стандарт TextureView
(который обеспечивает SurfaceTexture
где буфер видео сбрасывается). Я инициализирую воспроизведение мультимедиа только после того, как моя Activity закончила onResume() и когда доступна SurfaceTexture. Вместо того, чтобы создавать подклассы TextureView, я просто вызываю setSurfaceTexture (так как он открыт) в моем фрагменте, как только я получаю ссылку на SurfaceTexture. При изменении конфигурации сохраняются только две вещи: ссылка MediaPlayer и ссылка SurfaceTexture.
Я загрузил источник моего примера проекта на Github. Не стесняйтесь взглянуть!
Я знаю, что этот вопрос уже давно устарел, но я смог заставить его работать в моем приложении без пропуска. Проблема в том, что поверхность разрушается (убивая любой буфер, который у нее был). Это может решить не все ваши проблемы, потому что оно нацелено на API 16, но вы можете управлять своим собственным SurfaceTexture
внутри вашего обычая TextureView
где видео нарисовано:
private SurfaceTexture mTexture;
private TextureView.SurfaceTextureListener mSHCallback =
new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height) {
mTexture = surface;
mPlayer.setSurface(new Surface(mTexture));
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height) {
mTexture = surface;
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mTexture = surface;
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
mTexture = surface;
}
};
ключ возвращает ложь в onSurfaceTextureDestroyed
и держась за mTexture. Когда вид снова присоединяется к окну, вы можете установить поверхность-текстуру:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mTexture != null) {
setSurfaceTexture(mTexture);
}
}
Это позволяет моему мнению продолжать воспроизводить видео именно с того места, где оно было остановлено.
Я решил эту проблему, не предотвращая разрушение поверхности.
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
surfaceDestroyed = true
currentPosition = mediaPlayer?.currentPosition
return true
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
if (surfaceDestroyed) {
surfaceDestroyed = false
mediaPlayer?.setSurface(Surface(surface))
currentPosition?.let { mediaPlayer?.seekTo(it, MediaPlayer.SEEK_CLOSEST)
} else {
// do normal init
}