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
      }
Другие вопросы по тегам