Анализ кода синхронизации аудио и видео Android ExoPlayer
Анализ кода синхронизации аудио и видео Android ExoPlayer

1. Синхронизация аудио и видео.

1.1 Что такое синхронизация аудио и видео?

Аудиовизуальная синхронизация направлена ​​на использование опорных часов для согласования времени воспроизведения аудио, видео, текстов песен и т. д., чтобы гарантировать синхронизацию изображения и звука. При разработке аудио- и видеоплееров синхронизация аудио и видео является очень важной задачей, которая напрямую влияет на аудиовизуальный опыт пользователя.

Однако синхронизация аудио и видео включает в себя множество методов, и каждый метод отличается в зависимости от потребностей сцены. В настоящее время основная синхронизация аудио и видео обычно основана на методе Audio Master. Человеческое тело более чувствительно к звуку, чем к зрению. Вообще говоря, человеческий мозг все равно будет думать, что изображение плавное, если видеокадр слегка сдвинут. Однако, что касается звука, некоторые пропадания кадров могут отражать очевидные паузы или проблемы с шумом, что является одной из основных причин, почему основные проигрыватели сосредотачиваются на звуке для аудио и видео.

Конечно, когда речь идет о приложениях в разных отраслях, это не обязательно фиксированный метод. В некоторых областях даже не требуется синхронизация аудио и видео (например, некоторые игровые специальные эффекты). Их необходимо настроить и разработать в соответствии с требованиями. сцена Хороший игрок часто оттачивался в течение длительного периода времени.

1.2 Стандарты синхронизации аудио и видео

Международный союз электросвязи о 1998 Ежегодный пересмотр «ITU-R BT.1359-1》,противСтандарты синхронизации аудио и видео для телевизионных передач,Этот стандарт используется до сих пор.,В то же время сфера применения также расширилась и теперь включает прямую трансляцию через Интернет.

Рисунок: Стандарт восприятия задержки звука TU-R BT.1359-1.

Отклонение, приемлемое для пользователей:

  • Незаметно для пользователей: -100 мс ~ 25 мс.
  • Распознавание пользователя: –125 мс & 45ms
  • Максимальный диапазон отклонения, принимаемый пользователями: более -185 мс. & меньше, чем 90ms

Отклонения, неприемлемые для пользователей

  • Неприемлемо для пользователей: меньше, чем-185ms & больше, чем 90ms

1.3 Основная логика синхронизации аудио и видео

Для синхронизации основного аудио и видео используется Audio Master или независимые часы. Звук воспроизводится с постоянной скоростью, а метод воспроизведения видео контролируется ходом воспроизведения звука.

Предположим, что порог отображения синхронизации видео — syncTime, порог исключения — unexpectTime, syncTime должен быть больше unexpectTime, а время кадра декодирования видео — PTS.

  • -syncTime <= PTS<=syncTime Отправить дисплей
  • PTS < syncTime Удаление видеокадров
  • PTS >= unexpectTime отказаться от кадра адаптации
  • syncTime <= PTS <= unexpectTime Соответствующий поток сна используется для обеспечения соответствующей частоты отображения.

2. Распространенные методы синхронизации звука

Общие методы синхронизации

【1】Получите время воспроизведения аудио, затем выполните поиск от позиции воспроизведения видео к позиции воспроизведения аудио, а затем выполните поиск аудио к позиции видео.

Этот метод по существу вызывает задержку как в изображении, так и в видео. Причина двойного поиска заключается в том, что неопределенность GOP видео и поиск ключевых кадров более сложны, чем поиск видео. ожидания и его нужно искать заново. Звук полностью обработан.

преимущество:

  • Реализация проста, просто вызовите метод поиска.

недостаток:

  • Очень плохой опыт,видео и аудио каждый раз будут иметь очевидную задержку.,Буферизация будет длительной.

【2】Получите время воспроизведения аудио или видео и позвольте тому, кто играет быстрее, подождать, пока позиции не выровняются.

Рассчитайте разницу во времени, чем быстрее человек ждет (или делает паузу), Возобновите работу после выравнивания разницы во времени

преимущество:

  • Сложность средняя, ​​нужно залипать только Аудио или видео.

недостаток:

  • Управление более сложное и требует разумной степени детализации по времени для определения и определения положения.
  • На одной стороне аудио или видео может наблюдаться очевидная задержка или буферизация, если текущая позиция воспроизведения сильно отличается от целевой позиции. Намного выше.
  • Следует избегать таких операций, как приостановка и буферизация.

【3】видеопропущенные кадры&видео Ожидание выравнивания

Этот метод, как правило, является распространенным методом реализации основного проигрывателя. В настоящее время все основные проигрыватели, такие как MediaPlayer, ExoPlayer и iJkPlayer, реализуют этот метод. Если видео быстрое, используйте метод [2]. сделать видео Подождите, если видео медленное, видео будет терять кадры для достижения синхронизации.

преимущество:

  • Впечатления стали лучше, и аудио это никак не повлияет.

недостаток:

  • Обработка декодирования и потери кадров относительно сложна.
  • Если видео намного быстрее, чем аудио, видео всегда будет приостанавливаться.
  • Если видео намного медленнее, чем аудио, возможна очевидная потеря кадров.

【4】Синхронизация скорости

Время воспроизведения звука также используется в качестве стандарта. Если вы измените скорость воспроизведения видео, звук никак не повлияет. Видеоэкран будет немного двигаться и воспроизводиться быстрее. Для обычных пользователей это может считаться обычным экраном. .

Однако в области Интернета вещей такие производители, как Rockchip, AllWinner и HiSilicon, модифицируют базовый MediaPlayer. В настоящее время этот метод имеет множество проблем в некоторых безымянных проигрывателях. Например, некоторые MediaPlayer вызывают onCompletion сразу после вызова Seek, а некоторые вызывают onCompletion. после onError фрагментация очень серьезная, и даже некоторые коды состояния игрока являются частными.

преимущество:Лучший опыт,видео Медленнее, если едешь быстро видео,видеомедленное времявидеоускоряться

недостаток: Должен быть совместим с различными состояниями игрока.,Логика управления относительно сложна.,MediaPlayer, когда множитель равен 0 Будет считаться, что была вызвана пауза. Если множитель больше 0, будет считаться, что было вызвано возобновление.

Здесь мы кратко поговорим о конкретной реализации синхронизации переменной скорости.

  • S *speed*1 - S*1 = GapOffset (векторная формула, S - миллисекунды, 1 - стандартная скорость равна 1, GapOffset - разница во времени, обратите внимание, что это единица миллисекунды)
  • соответствовать скорости >= Ситуация 0,25, пусть S <=5000,Если вы не удовлетворены,SМожетбольше, чем5000, но скорость не может быть меньше, чем0.25 (Уведомление :speed не может быть 0, иначе MediaPlayer подумает, что вызывается пауза)
  • После превышения времени S вернитесь к исходной скорости.
  • Спасибо Медиаплееру Установите настройку скорости, возможно, как Возобновите и приостановите обработку, чтобы существование вернулось к исходной скорости перед вызовом возобновления и паузы.

3. Экзоплеер Анализ синхронизации аудио и видео

Возвращаясь к теме этой статьи, давайте проанализируем метод синхронизации аудио и видео ExoPlayer, чтобы использовать этот механизм для достижения многопользовательской синхронизации в некоторых сценариях. Как и обычные проигрыватели, ExoPlayer также использует метод синхронизации на основе звука. В этой статье мы объясним это шаг за шагом.

3.1 Почему ExoPlayer основан на аудио?

Сам исходный код ExoPlayer имеет часы. Существует два основных тактовых сигнала: один — это часы, реализованные MediaCodecAudioRenderer, а другой — StandaloneMediaClock. Первый действует как Audio Master, обеспечивая время воспроизведения звука для видео, а второй использует естественное время в качестве резервных часов для обеспечения времени воспроизведения для различных рендеров.

В ExoPlayer в реализации Audio Master есть два основных класса: com.google.android.exoplayer2.audio.AudioTrackPositionTracker и com.google.android.exoplayer2.audio.AudioTimestampPoller.

Преимущество использования этих двух классов заключается в том, что они позволяют избежать двух проблем AudioTrack#getPlaybackHeadPosition. Одна из них заключается в том, что он может только увеличиваться, но не может двигаться назад, например, поиск вперед (переход назад). Вторая причина заключается в том, что используется какое-то безымянное оборудование. отрицательно влияет на AudioTrack#getPlaybackHeadPosition При адаптации возникает проблема с передним и задним джиттером, что просто губительно для синхронизации аудио и видео. Для решения этой проблемы было реализовано аварийное восстановление, чтобы воспроизведение оставалось «плавным».

Здесь мы в основном анализируем реализацию Audio Master. Помимо расчета позиции воспроизведения естественных часов StandaloneMediaClock, процесс синхронизации аудио и видео в основном аналогичен методу Audio Master. В ExoPlayer метод com.google.android.exoplayer2.audio.BaseRenderer#getMediaClock пуст, но в подклассах видео по-прежнему возвращает значение null, и реализуется только средство рендеринга звука.

com.google.android.exoplayer2.audio.MediaCodecAudioRenderer#getMediaClock()。

Язык кода:javascript
копировать
@Override
@Nullable
public MediaClock getMediaClock() {
  return this;
}

Это также доказывает, что при наличии аудиорендерера звук будет преобладать. Конечно, если аудиорендерера нет, в ExoPlayer будут использоваться естественные часы StandaloneMediaClock. Ниже приведен выбор часов рендеринга. Рендереры с несуществующими или пустыми часами в конечном итоге исключаются, и существование нескольких часов не допускается.

com.google.android.exoplayer2.DefaultMediaClock

Язык кода:javascript
копировать
public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException {
  @Nullable MediaClock rendererMediaClock = renderer.getMediaClock(); //Только Аудиоиз не пуст
  if (rendererMediaClock != null && rendererMediaClock != rendererClock) { 
    if (rendererClock != null) {
      throw ExoPlaybackException.createForUnexpected(
          new IllegalStateException("Multiple renderer media clocks enabled."));
    }
    this.rendererClock = rendererMediaClock;
    this.rendererClockSource = renderer;
    rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
  }
}

3.2 Роль MediaClock

MediaClock — важный компонент процесса воспроизведения в ExoPlayer. Он имеет только две основные логики: одна — для регулировки скорости воспроизведения, а другая — для получения времени воспроизведения.

Язык кода:javascript
копировать
public interface MediaClock {

  /**
   * Returns the current media position in microseconds.
   */
  long getPositionUs();

  /**
   * Attempts to set the playback parameters. The media clock may override the speed if changing the
   * playback parameters is not supported.
   *
   * @param playbackParameters The playback parameters to attempt to set.
   */
  void setPlaybackParameters(PlaybackParameters playbackParameters);

  /** Returns the active playback parameters. */
  PlaybackParameters getPlaybackParameters();
}

К сожалению, в ExoPlayer сложно передать пользовательский MediaClock извне. Итак, если вы хотите передать пользовательский MediaClock извне, как этого добиться? Ответ на этот вопрос приведен ниже.

3.3 Обновление времени синхронизации

Для начала давайте посмотрим на вызов метода синхронизации

Язык кода:javascript
копировать
private void updatePlaybackPositions() throws ExoPlaybackException {
  MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
  if (playingPeriodHolder == null) {
    return;
  }

  // Update the playback position.
  long discontinuityPositionUs =
      playingPeriodHolder.prepared
          ? playingPeriodHolder.mediaPeriod.readDiscontinuity()
          : C.TIME_UNSET;
  if (discontinuityPositionUs != C.TIME_UNSET) {
    resetRendererPosition(discontinuityPositionUs);
    // A MediaPeriod may report a discontinuity at the current playback position to ensure the
    // renderers are flushed. Only report the discontinuity externally if the position changed.
    if (discontinuityPositionUs != playbackInfo.positionUs) {
      playbackInfo =
          handlePositionDiscontinuity(
              playbackInfo.periodId,
              discontinuityPositionUs,
              playbackInfo.requestedContentPositionUs);
      playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
    }
  } else {
    rendererPositionUs =
        mediaClock.syncAndGetPositionUs(  
            /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
    long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
    maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
    playbackInfo.positionUs = periodPositionUs;
  }

  // Update the buffered position and total buffered duration.
  MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
  playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
  playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
}

Из кода мы видим, что syncAndGetPositionUs() из MediaClock используется для получения времени синхронизации. Этот метод на самом деле находится в DefaultMediaClock и принадлежит подклассу MediaClock. Получите точку времени воспроизведения RendererClock или StandoloneMediaClock. Обратите внимание, что это не синхронизация видео, а просто получение времени синхронизации, а получение позиции звука после синхронизации с системным временем. Что касается syncAndGetPositionUs, нам не нужно обращать на это внимание. В основном это делается для исправления прерывистой обработки времени.

3.4 Как синхронизировать позицию воспроизведения звука с видео?

Мы можем рассмотреть вызов метода doSomeWork(). Этот метод регулярно вызывается в ExoPlayer для управления состоянием воспроизведения, загрузкой ресурсов и синхронизацией аудио и видео. Вот простой перехват кода ключа. .

Язык кода:javascript
копировать
private void doSomeWork() throws ExoPlaybackException, IOException {


  updatePlaybackPositions();  //Обновляем местоположение

//...................//
   boolean renderersEnded = true;
   boolean renderersReadyOrEnded = true;

  for(Render render : enabledRenderers){    //Удобство для всех активных на данный момент существовавших использовать рендерер
 
      // TODO: Each renderer should return the maximum delay before which it wishes to be called
      // again. The minimum of these values should then be used as the delay before the next
      // invocation of this method.
       renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);  //синхронный Расположение       renderersEnded = renderersEnded && renderer.isEnded();  //Определяем, завершен ли рендеринг
       boolean rendererReadyOrEnded = renderer.isReady() || readerer.isEnded() || rendererWaitingForNextStream(rendered);

       if(!rendererReadyOrEnded){  //Если он не находится в состоянии «Готово» или «Конечное состояние», это означает, что может возникнуть проблема с декодированием данных рендерером.
          renderer.maybeThrowStreamError();
       }
       renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded;
  }


}

Все включенные рендереры в коде будут синхронизированы. Это не только аудио. ExoPlayer обладает хорошей масштабируемостью и может поддерживать несколько рендереров одновременно. Здесь мы сосредоточимся только на синхронизации видеорендерера. В конце концов, управление видео является относительно сложным.

3.4 Как синхронизировать видео

существовать MediaCodecVideoRender тяжелый, рендеринг () ->drainOutputBuffer -> processOutputBuffer Проходит время, в конечном итоге существуют processOuputBuffer в обработке.

Язык кода:javascript
копировать
 protected boolean processOutputBuffer(
      long positionUs,
      long elapsedRealtimeUs,
      @Nullable MediaCodec codec,
      @Nullable ByteBuffer buffer,
      int bufferIndex,
      int bufferFlags,
      int sampleCount,
      long bufferPresentationTimeUs,
      boolean isDecodeOnlyBuffer,
      boolean isLastBuffer,
      Format format)
      throws ExoPlaybackException {
    Assertions.checkNotNull(codec); // Can not render video without codec

    if (initialPositionUs == C.TIME_UNSET) {
      initialPositionUs = positionUs;
    }

   //Получаем время смещения потока данных
    long outputStreamOffsetUs = getOutputStreamOffsetUs();
    //Рассчитываем текущее количество отображаемых точек для отправки
    long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;

    if (isDecodeOnlyBuffer && !isLastBuffer) {
    //Если вы участвуете только в декодировании и не упоминаете последний буфер, все данные не будут отправлены на отображение. Наконец, вызовите codec.releaseOutputBuffer(index, false) ,false Указывает на отсутствие дисплея
      skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
      
      return true;
    }

     // Вычислить смещение времени по синхронным часам.
    long earlyUs = bufferPresentationTimeUs - positionUs;
    if (surface == dummySurface) {
      // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
      //Если это поверхность по умолчанию, синхронизация не выполняется. Если буфер позже времени воспроизведения, SKIP отбрасывается напрямую. В противном случае буфер все равно сохраняется после достижения времени.
      if (isBufferLate(earlyUs)) {
        skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
        updateVideoFrameProcessingOffsetCounters(earlyUs);
        return true;
      }
      return false;
    }

    long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
    long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs;
    //Определяем, находится ли он в состоянии воспроизведения
    boolean isStarted = getState() == STATE_STARTED;
    //Определяем, пора ли рендерить первый кадр
    boolean shouldRenderFirstFrame =
        !renderedFirstFrameAfterEnable
            ? (isStarted || mayRenderFirstFrameAfterEnableIfNotStarted)
            : !renderedFirstFrameAfterReset;
           
          
            
    // Don't force output until we joined and the position reached the current stream.
    //Определяем, следует ли принудительно рендерить, в основном, если это первый кадр или предыдущий кадр отображает Превосходить100 мс и рано уменьшить, Принудительный рендеринг займет более 30 мс. В других случаях принудительный рендеринг не требуется. Подробности см. в разделе mustForceRenderOutputBuffer(). Исходный код
    boolean forceRenderOutputBuffer =
        joiningDeadlineMs == C.TIME_UNSET
            && positionUs >= outputStreamOffsetUs
            && (shouldRenderFirstFrame
                || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs)));
                
    if (forceRenderOutputBuffer) {
      long releaseTimeNs = System.nanoTime();
      notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);
      if (Util.SDK_INT >= 21) {
        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs);
      } else {
        renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
      }
      updateVideoFrameProcessingOffsetCounters(earlyUs);
      return true;
    }

     //Если видео еще официально не воспроизводилось, вернитесь напрямую
    if (!isStarted || positionUs == initialPositionUs) {
      return false;
    }
    
    //Следующее — раннее, чем 0из ситуации

    // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current
    // iteration of the rendering loop.
    long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs;
    earlyUs -= elapsedSinceStartOfLoopUs;

    // Compute the buffer's desired release time in nanoseconds.
    long systemTimeNs = System.nanoTime();
    long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);

    // Apply a timestamp adjustment, if there is one.
    long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(
        bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs);
  
  //Настраиваем время отображения
    earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;

//Определяем, следует ли пропускать или удалять кадры. Самая большая разница между пропуском и удалением кадров заключается в том, что последнее будет уведомлено за пределами игрока.
    boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET;
   
    if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)
        && maybeDropBuffersToKeyframe(
            codec, bufferIndex, presentationTimeUs, positionUs, treatDroppedBuffersAsSkipped)) {
      //Если кадр переброшен в ключевой кадр из позиции, здесь необходимо уделить особое внимание. Если логика достигает этой точки, декодер может перезапуститься и может возникнуть проблема с черным экраном.
      return false;
    } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {
      if (treatDroppedBuffersAsSkipped) {
        skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
      } else {
        dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
      }
      updateVideoFrameProcessingOffsetCounters(earlyUs);
      return true;
    }

    if (Util.SDK_INT >= 21) {
      // Let the underlying framework time the release.
     // android 5.0+ Версию только нужно прислать с временем показа меньше, чем50мс будет отображаться
      if (earlyUs < 50000) {
        notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
        renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
        updateVideoFrameProcessingOffsetCounters(earlyUs);
        return true;
      }
    } else {
      // We need to time the release ourselves.
      if (earlyUs < 30000) {
      //android 4.4 До Версия меньше, чем30мс для отправки на дисплей, но если время больше, чем11мс, затем уменьшите задержку соответствующим образом
        if (earlyUs > 11000) {
          // We're a little too early to render the frame. Sleep until the frame can be rendered.
          // Note: The 11ms threshold was chosen fairly arbitrarily.
          try {
            // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
           //Здесь вообще-то экзоплеер надеется, что из максимально обеспечит ритм 10мс, если Превосходить хотя бы спит 1ms
            Thread.sleep((earlyUs - 10000) / 1000);

          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
          }
        }
        notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
        renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
        updateVideoFrameProcessingOffsetCounters(earlyUs);
        return true;
      }
    }

     //Вернем false. Вообще говоря, основная причина в том, что время отправки дисплея еще не наступило, поэтому неотправленный буфер дисплея все еще сохраняется.
    // We're either not playing, or it's not time to render the frame yet.
    return false;
  }

Приведенный выше анализ кода написан прямо в комментариях. Основная логика такова: получить калиброванное время с помощью длительного метода выполнения. EarlyUs, следующий шаг будет основан на earlyUs Чтобы отбросить кадры, пропустите кадры или дождитесь декодирования звука. если earlyUs час Разница положительная,Представляет собой видеокадр, который должен отображаться после текущего системного времени.,Другими словами,Репрезентативный видеокадр появился раньше,Напротив,Если разница во времени отрицательна,представлятьвидео Рамка должнасуществовать Текущая системачасмежду Доотображаться,Другими словами,Репрезентативный видеокадр пришел с опозданием. Если Превосходить определённо из лимита,Этот кадр видео появился слишком поздно,затем выбрось этот кадр,Не отображается. Согласно заданному пороговому значению,видеосоотношение кадров заранее заданочасмежду来из Еще рано 30~50ms Выше, Андроид 5.0 или выше могут контролировать время отображения, если оно превышает его, оно не будет отображаться и будет ждать следующей запланированной синхронизации, если это Android; До 4.4 входил в ожидание, и Андроид Внутренняя логика в ExoPlayer версии 4.4 явно ожидает синхронизацию с частотой 10мс, иначе она будет отправляться на дисплей напрямую.

4. Краткое описание процесса синхронизации аудио и видео ExoPlayer

Благодаря этой статье мы знаем, что весь синхронный процесс регулярно запускается из,Чтобы убедиться, что это активный метод обнаружения, его проводят. существуют некоторые бизнес-серединаиз Выход Audio и ExoPlayer разделены,Мы рассмотрим, как использовать синхронный аудиоплеер ExoPlayerсерединаизвидео рендерер.,Но ExoPlayer легко масштабируется.,Мы можем воспроизвести его на ExoPlayer, настроив часы.,конечно Предпосылкой является знакомствоExoPlayerизаудиовизуальныйсинхронныйиз Процесс вызова。

Рисунок: Основной процесс вызова синхронизации аудио и видео.

5. Как использовать настроенный MediaClock в бизнесе?

ExoPlayer обладает хорошей масштабируемостью, но если вы передаете параметры, сложно передать настроенный MediaClock.

Но разработчики ExoPlayer также предоставляют другой канал, то есть через com.google.android.exoplayer2.DefaultRenderersFactory#createRenderers мы можем наследовать DefaultRenderersFactory, переопределить реализацию, связанную с createRenderers, и передать наш настроенный MediaClock соответствующему рендереру. Как упоминалось ранее, базовый класс Renderer com.google.android.exoplayer2.BaseRenderer#getMediaClock поддерживает пользовательский MediaClock.

Язык кода:javascript
копировать
public class MusicMediaCodecVideoRenderer extends MediaCodecVideoRenderer {

    private KtvMediaClock mediaClock;

    public MusicMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
        super(context, mediaCodecSelector);
    }

    public MusicMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs) {
        super(context, mediaCodecSelector, allowedJoiningTimeMs);
    }

    public MusicMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs, @Nullable  Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
        super(context, mediaCodecSelector, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
    }

    public MusicMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs, boolean enableDecoderFallback, @Nullable Handler eventHandler, @Nullable  VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
        super(context, mediaCodecSelector, allowedJoiningTimeMs, enableDecoderFallback, eventHandler, eventListener, maxDroppedFramesToNotify);
    }

    public void setMediaClock(KtvMediaClock mediaClock) {
        this.mediaClock = mediaClock;
    }

    @Override
    protected void onStarted() {
        super.onStarted();
        if(this.mediaClock != null) {
            this.mediaClock.start();
        }
    }

    @Override
    protected void onStopped() {
        super.onStopped();
        if (this.mediaClock != null) {
            this.mediaClock.stop();
        }
    }

    @Override
    protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
        super.onPositionReset(positionUs, joining);
        if (this.mediaClock != null) {
            this.mediaClock.resetPosition(positionUs);
        }
    }

    @Override
    public MediaClock getMediaClock() {
        return mediaClock;
    }

}

6. Вложение: О дефектах AudioTrack

Мы знаем, что AudioTrack сам по себе не поддерживает Seek. Кроме того, из-за особенностей версии системы обработка прогресса AudioTrack не очень идеальна. Обычно мы можем использовать только offset + getPlaybackHeadPosition для выполнения логики Seek, а затем вычислять время через смещение.

Однако,существуют Есть много проблем с вычислением времени через AudioTack#getPlaybackHeadPosition на некоторых устройствах.,Потому что существует много трудностей в сохранении существования,В основном задержка обработки,На устройстве наблюдается сильное дрожание при попытке сохранить существующее состояние из PlaybackHeadPosition.,поэтому,Очень необходимо использовать антивибрацию часов.

Следующая программа представляет собой логику, имитирующую работу AudioTrack#write и AudioTrack#getPlaybackHeadPosition.

Язык кода:javascript
копировать
public class AudioClockOutputDevice extends AudioOutput{

    private final AudioParams params;
    private boolean isResumed = false;
    int playState = 0;
    int writeFrameSize = 0;

    public AudioClockOutputDevice(AudioParams params) {
        this.params = params;
        playState = AudioPlayState.PLAYSTATE_NEW;
    }
    @Override
    public void start() throws IOException {
        isResumed = true;
        playState = AudioPlayState.PLAYSTATE_PLAYING;
    }

    @Override
    public void stop() throws IOException {
        isResumed = false;
        playState = AudioPlayState.PLAYSTATE_STOPPED;
    }

    @Override
    public void resume() throws IOException {
        isResumed = true;
        playState = AudioPlayState.PLAYSTATE_PLAYING;

    }

    @Override
    public void pause() throws IOException {
        isResumed = false;
        playState = AudioPlayState.PLAYSTATE_PAUSED;
    }

    @Override
    public void flush() throws IOException {

    }

    @Override
    public void release() throws IOException {
        playState = AudioPlayState.PLAYSTATE_STOPPED;
    }

    @Override
    public int write(AudioFrame audioFrame) throws IOException {
        int toTimeMillis = AudioUtils.byteSizeToTimeMillis(audioFrame.size, (int) this.params.sampleRate, this.params.channelCount, this.params.bitDepth);
        int totalWrittenFrameSize = writeFrameSize + audioFrame.size;

        if (toTimeMillis > 0) {
            long targetTimeMillis = SystemClock.uptimeMillis() + toTimeMillis;
            int bytePerMilliseconds = audioFrame.size / toTimeMillis;
            final long STEP = 10; 
            while (targetTimeMillis >= (SystemClock.uptimeMillis() + STEP)) {
                try {
                    TimeUnit.MILLISECONDS.sleep(STEP);
                } catch (Throwable e) {
                    e.printStackTrace();
                }

                int result  = (int) (writeFrameSize + STEP * bytePerMilliseconds);
                if(result > totalWrittenFrameSize){
                    result = totalWrittenFrameSize;
                }
                writeFrameSize = result;
            }
        }
        writeFrameSize = totalWrittenFrameSize;
        return audioFrame.size;
    }

    @Override
    public void setVolume(float v) throws IOException {
    }

    @Override
    public void setMicVolume(float v) throws IOException {
    }

    @Override
    public int getPlaybackHeadPosition() throws IOException {
        return AudioUtils.byteSizeToSamplePosition(writeFrameSize,this.params.channelCount,this.params.bitDepth);
    }

    @Override
    public int getPlaybackBufferSize() throws IOException {
        return 4096;
    }

    @Override
    public int getAudioSessionId() throws IOException {
        return 0;
    }

    @Override
    public int getPlayState() throws IOException {
        return playState;
    }
}

Это всего лишь простая симуляция. Реальная логика вычислений относительно сложнее. Конечная цель — гарантировать, что объем данных sampleRate *channelCount * bitDepth будет использован в течение 1 секунды. Кстати, как решить проблему дрожания времени AudioTrack?

Одним из возможных способов является обнаружение джиттера.,При достижении определенного порога метод getPlayHeadPosition не вызывается.,Вместо этого рассчитывайте прогресс с помощью настраиваемых часов.,Вызывается только тогда, когда существуетпауза, воспроизведение, возобновление,конечно,Также откажитесь от вызова этого метода после того, как существующий getPlayHeadPosition возвращает значение больше, чем 0.,В противном случае задержка (Latecy) может вызвать новую несинхронную проблему.

boy illustration
Углубленный анализ переполнения памяти CUDA: OutOfMemoryError: CUDA не хватает памяти. Попыталась выделить 3,21 Ги Б (GPU 0; всего 8,00 Ги Б).
boy illustration
[Решено] ошибка установки conda. Среда решения: не удалось выполнить первоначальное зависание. Повторная попытка с помощью файла (графическое руководство).
boy illustration
Прочитайте нейросетевую модель Трансформера в одной статье
boy illustration
.ART Теплые зимние предложения уже открыты
boy illustration
Сравнительная таблица описания кодов ошибок Amap
boy illustration
Уведомление о последних правилах Points Mall в декабре 2022 года.
boy illustration
Даже новички могут быстро приступить к работе с легким сервером приложений.
boy illustration
Взгляд на RSAC 2024|Защита конфиденциальности в эпоху больших моделей
boy illustration
Вы используете ИИ каждый день и до сих пор не знаете, как ИИ дает обратную связь? Одна статья для понимания реализации в коде Python общих функций потерь генеративных моделей + анализ принципов расчета.
boy illustration
Используйте (внутренний) почтовый ящик для образовательных учреждений, чтобы использовать Microsoft Family Bucket (1T дискового пространства на одном диске и версию Office 365 для образовательных учреждений)
boy illustration
Руководство по началу работы с оперативным проектом (7) Практическое сочетание оперативного письма — оперативного письма на основе интеллектуальной системы вопросов и ответов службы поддержки клиентов
boy illustration
[docker] Версия сервера «Чтение 3» — создайте свою собственную программу чтения веб-текста
boy illustration
Обзор Cloud-init и этапы создания в рамках PVE
boy illustration
Корпоративные пользователи используют пакет регистрационных ресурсов для регистрации ICP для веб-сайта и активации оплаты WeChat H5 (с кодом платежного узла версии API V3)
boy illustration
Подробное объяснение таких показателей производительности с высоким уровнем параллелизма, как QPS, TPS, RT и пропускная способность.
boy illustration
Удачи в конкурсе Python Essay Challenge, станьте первым, кто испытает новую функцию сообщества [Запускать блоки кода онлайн] и выиграйте множество изысканных подарков!
boy illustration
[Техническая посадка травы] Кровавая рвота и отделка позволяют вам необычным образом ощипывать гусиные перья! Не распространяйте информацию! ! !
boy illustration
[Официальное ограниченное по времени мероприятие] Сейчас ноябрь, напишите и получите приз
boy illustration
Прочтите это в одной статье: Учебник для няни по созданию сервера Huanshou Parlu на базе CVM-сервера.
boy illustration
Cloud Native | Что такое CRD (настраиваемые определения ресурсов) в K8s?
boy illustration
Как использовать Cloudflare CDN для настройки узла (CF самостоятельно выбирает IP) Гонконг, Китай/Азия узел/сводка и рекомендации внутреннего высокоскоростного IP-сегмента
boy illustration
Дополнительные правила вознаграждения амбассадоров акции в марте 2023 г.
boy illustration
Можно ли открыть частный сервер Phantom Beast Palu одним щелчком мыши? Супер простой урок для начинающих! (Прилагается метод обновления сервера)
boy illustration
[Играйте с Phantom Beast Palu] Обновите игровой сервер Phantom Beast Pallu одним щелчком мыши
boy illustration
Maotouhu делится: последний доступный внутри страны адрес склада исходного образа Docker 2024 года (обновлено 1 декабря)
boy illustration
Кодирование Base64 в MultipartFile
boy illustration
5 точек расширения SpringBoot, супер практично!
boy illustration
Глубокое понимание сопоставления индексов Elasticsearch.
boy illustration
15 рекомендуемых платформ разработки с нулевым кодом корпоративного уровня. Всегда найдется та, которая вам понравится.
boy illustration
Аннотация EasyExcel позволяет экспортировать с сохранением двух десятичных знаков.