1.1 Что такое синхронизация аудио и видео?
Аудиовизуальная синхронизация направлена на использование опорных часов для согласования времени воспроизведения аудио, видео, текстов песен и т. д., чтобы гарантировать синхронизацию изображения и звука. При разработке аудио- и видеоплееров синхронизация аудио и видео является очень важной задачей, которая напрямую влияет на аудиовизуальный опыт пользователя.
Однако синхронизация аудио и видео включает в себя множество методов, и каждый метод отличается в зависимости от потребностей сцены. В настоящее время основная синхронизация аудио и видео обычно основана на методе Audio Master. Человеческое тело более чувствительно к звуку, чем к зрению. Вообще говоря, человеческий мозг все равно будет думать, что изображение плавное, если видеокадр слегка сдвинут. Однако, что касается звука, некоторые пропадания кадров могут отражать очевидные паузы или проблемы с шумом, что является одной из основных причин, почему основные проигрыватели сосредотачиваются на звуке для аудио и видео.
Конечно, когда речь идет о приложениях в разных отраслях, это не обязательно фиксированный метод. В некоторых областях даже не требуется синхронизация аудио и видео (например, некоторые игровые специальные эффекты). Их необходимо настроить и разработать в соответствии с требованиями. сцена Хороший игрок часто оттачивался в течение длительного периода времени.
1.2 Стандарты синхронизации аудио и видео
Международный союз электросвязи о 1998 Ежегодный пересмотр «ITU-R BT.1359-1》,противСтандарты синхронизации аудио и видео для телевизионных передач,Этот стандарт используется до сих пор.,В то же время сфера применения также расширилась и теперь включает прямую трансляцию через Интернет.
Рисунок: Стандарт восприятия задержки звука TU-R BT.1359-1.
Отклонение, приемлемое для пользователей:
Отклонения, неприемлемые для пользователей
1.3 Основная логика синхронизации аудио и видео
Для синхронизации основного аудио и видео используется Audio Master или независимые часы. Звук воспроизводится с постоянной скоростью, а метод воспроизведения видео контролируется ходом воспроизведения звука.
Предположим, что порог отображения синхронизации видео — syncTime, порог исключения — unexpectTime, syncTime должен быть больше unexpectTime, а время кадра декодирования видео — PTS.
2. Распространенные методы синхронизации звука
Общие методы синхронизации
【1】Получите время воспроизведения аудио, затем выполните поиск от позиции воспроизведения видео к позиции воспроизведения аудио, а затем выполните поиск аудио к позиции видео.
Этот метод по существу вызывает задержку как в изображении, так и в видео. Причина двойного поиска заключается в том, что неопределенность GOP видео и поиск ключевых кадров более сложны, чем поиск видео. ожидания и его нужно искать заново. Звук полностью обработан.
преимущество:
недостаток:
【2】Получите время воспроизведения аудио или видео и позвольте тому, кто играет быстрее, подождать, пока позиции не выровняются.
Рассчитайте разницу во времени, чем быстрее человек ждет (или делает паузу), Возобновите работу после выравнивания разницы во времени
преимущество:
недостаток:
【3】видеопропущенные кадры&видео Ожидание выравнивания
Этот метод, как правило, является распространенным методом реализации основного проигрывателя. В настоящее время все основные проигрыватели, такие как MediaPlayer, ExoPlayer и iJkPlayer, реализуют этот метод. Если видео быстрое, используйте метод [2]. сделать видео Подождите, если видео медленное, видео будет терять кадры для достижения синхронизации.
преимущество:
недостаток:
【4】Синхронизация скорости
Время воспроизведения звука также используется в качестве стандарта. Если вы измените скорость воспроизведения видео, звук никак не повлияет. Видеоэкран будет немного двигаться и воспроизводиться быстрее. Для обычных пользователей это может считаться обычным экраном. .
Однако в области Интернета вещей такие производители, как Rockchip, AllWinner и HiSilicon, модифицируют базовый MediaPlayer. В настоящее время этот метод имеет множество проблем в некоторых безымянных проигрывателях. Например, некоторые MediaPlayer вызывают onCompletion сразу после вызова Seek, а некоторые вызывают onCompletion. после onError фрагментация очень серьезная, и даже некоторые коды состояния игрока являются частными.
преимущество:Лучший опыт,видео Медленнее, если едешь быстро видео,видеомедленное времявидеоускоряться
недостаток: Должен быть совместим с различными состояниями игрока.,Логика управления относительно сложна.,MediaPlayer, когда множитель равен 0 Будет считаться, что была вызвана пауза. Если множитель больше 0, будет считаться, что было вызвано возобновление.
Здесь мы кратко поговорим о конкретной реализации синхронизации переменной скорости.
3. Экзоплеер Анализ синхронизации аудио и видео
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()。
@Override
@Nullable
public MediaClock getMediaClock() {
return this;
}
Это также доказывает, что при наличии аудиорендерера звук будет преобладать. Конечно, если аудиорендерера нет, в ExoPlayer будут использоваться естественные часы StandaloneMediaClock. Ниже приведен выбор часов рендеринга. Рендереры с несуществующими или пустыми часами в конечном итоге исключаются, и существование нескольких часов не допускается.
com.google.android.exoplayer2.DefaultMediaClock
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. Он имеет только две основные логики: одна — для регулировки скорости воспроизведения, а другая — для получения времени воспроизведения.
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 Обновление времени синхронизации
Для начала давайте посмотрим на вызов метода синхронизации
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 для управления состоянием воспроизведения, загрузкой ресурсов и синхронизацией аудио и видео. Вот простой перехват кода ключа. .
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 в обработке.
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мс, иначе она будет отправляться на дисплей напрямую.
Благодаря этой статье мы знаем, что весь синхронный процесс регулярно запускается из,Чтобы убедиться, что это активный метод обнаружения, его проводят. существуют некоторые бизнес-серединаиз Выход Audio и ExoPlayer разделены,Мы рассмотрим, как использовать синхронный аудиоплеер ExoPlayerсерединаизвидео рендерер.,Но ExoPlayer легко масштабируется.,Мы можем воспроизвести его на ExoPlayer, настроив часы.,конечно Предпосылкой является знакомствоExoPlayerизаудиовизуальныйсинхронныйиз Процесс вызова。
Рисунок: Основной процесс вызова синхронизации аудио и видео.
ExoPlayer обладает хорошей масштабируемостью, но если вы передаете параметры, сложно передать настроенный MediaClock.
Но разработчики ExoPlayer также предоставляют другой канал, то есть через com.google.android.exoplayer2.DefaultRenderersFactory#createRenderers мы можем наследовать DefaultRenderersFactory, переопределить реализацию, связанную с createRenderers, и передать наш настроенный MediaClock соответствующему рендереру. Как упоминалось ранее, базовый класс Renderer com.google.android.exoplayer2.BaseRenderer#getMediaClock поддерживает пользовательский MediaClock.
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;
}
}
Мы знаем, что AudioTrack сам по себе не поддерживает Seek. Кроме того, из-за особенностей версии системы обработка прогресса AudioTrack не очень идеальна. Обычно мы можем использовать только offset + getPlaybackHeadPosition для выполнения логики Seek, а затем вычислять время через смещение.
Однако,существуют Есть много проблем с вычислением времени через AudioTack#getPlaybackHeadPosition на некоторых устройствах.,Потому что существует много трудностей в сохранении существования,В основном задержка обработки,На устройстве наблюдается сильное дрожание при попытке сохранить существующее состояние из PlaybackHeadPosition.,поэтому,Очень необходимо использовать антивибрацию часов.
Следующая программа представляет собой логику, имитирующую работу AudioTrack#write и AudioTrack#getPlaybackHeadPosition.
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) может вызвать новую несинхронную проблему.