Skip to content

RU RU 3 Playing media

J-RAP edited this page Nov 7, 2026 · 3 revisions

3. ВОСПРОИЗВЕДЕНИЕ МЕДИА

Note

Настраивается: player.defaultQuality, player.cacheBudget, player.watertx.cache и player.watertx.texturesBudget. Смотрите их комментарии в config/watermedia.toml.

3.1 СОЗДАНИЕ ПЛЕЕРА И УПРАВЛЕНИЕ ИМ

MediaPlayer player = PlayerAPI.player(mrl, 0,
        () -> PlayerAPI.glEngine(renderThread, renderExecutor),
        PlayerAPI::jsEngine);
if (player == null) {
    throw new IllegalStateException("Source is unavailable or its backend failed");
}
try {
    if (!player.start()) throw new IllegalStateException("Player refused to start");
} catch (RuntimeException | Error failure) {
    player.release();
    throw failure;
}

player возвращает null, если источник ещё разрешается, такого индекса нет, бэкенд отсутствует или конструирование бросает Exception (а вот Error пробрасывается дальше). Проверьте статус MRL, прежде чем повторять попытку.

  • Поставщики вызываются только при необходимости, а изображения никогда не вызывают аудиопоставщик.
  • Поставщик, возвращающий null, отключает этот вывод, например () -> null в качестве графического движка для воспроизведения только звука.
  • Никогда не делите один движок между плеерами.

TxMediaPlayer воспроизводит поддерживаемые изображения и анимации, FFMediaPlayer воспроизводит аудио и видео через FFmpeg, а ServerMediaPlayer представляет собой часы синхронизации без декодирования и нативных движков.

У всех трёх плееров одинаковые методы управления: они возвращают true, когда вызов сработал, и false, когда он был отклонён (сейчас это невозможно, плеер уже освобождён или скорость вне диапазона). Настройки вывода ничего не возвращают.

// PLAY, PAUSE AND STOP
boolean started = player.start();                 // PLAYS FROM THE BEGINNING; ALSO RESTARTS AN ENDED, STOPPED OR FAILED PLAYER
boolean startedPaused = player.startPaused();     // THE SAME, BUT LANDS PAUSED (SAFER THAN start() FOLLOWED BY pause())
boolean paused = player.pause();                  // false WHEN ALREADY PAUSED, ENDED, STOPPED OR FAILED; WHILE LOADING IT WAITS FOR THE PIPELINE
boolean resumed = player.resume();                // false UNLESS PAUSED
boolean pauseChanged = player.pause(wantPaused);  // pause() OR resume() FROM A FLAG, SUCH AS A CHECKBOX
boolean toggled = player.togglePlay();            // PAUSES, RESUMES, OR STARTS AN ENDED OR STOPPED PLAYER AGAIN
boolean stopped = player.stop();                  // STOPPED WITH time() AT 0; false WHEN ALREADY STOPPED OR NEVER STARTED

// MOVE THROUGH THE MEDIA (MILLISECONDS, CLAMPED TO THE DURATION)
boolean sought = player.seek(15_000);             // PRECISE; ON AN ENDED OR STOPPED PLAYER IT RESTARTS PAUSED AT THE TARGET
boolean soughtQuick = player.seekQuick(15_000);   // FASTER, LESS PRECISE ON VIDEO; THE SAME AS seek() ON IMAGES
boolean skipped = player.skipTime(-10_000);       // RELATIVE TO time(); NEGATIVE GOES BACK
boolean forwarded = player.forward();             // 5 SECONDS AHEAD (1 SECOND ON ANIMATIONS)
boolean rewound = player.rewind();                // 5 SECONDS BACK (1 SECOND ON ANIMATIONS)
boolean stepped = player.nextFrame();             // ONLY WHILE PAUSED AND NOT FOLLOWING A CLOCK
boolean steppedBack = player.previousFrame();     // ONLY WHILE PAUSED AND NOT FOLLOWING A CLOCK

// PLAYBACK OPTIONS
boolean speedChanged = player.speed(1.25f);       // (0, 4]; false ON LIVE STREAMS, JAVA SOUND OR A RATE THE AUDIO ENGINE REFUSES
boolean repeatChanged = player.repeat(true);      // false ONLY AFTER release()
boolean trackChanged = player.audioTrack(track);  // SEE AUDIO TRACKS AND LANGUAGES
boolean placed = player.spatialAudio(position);   // SEE SPATIAL AUDIO; false WITHOUT A SPATIAL OPENAL ENGINE

// OUTPUT SETTINGS: ALWAYS TAKEN, SO THEY RETURN NOTHING
player.volume(50);                                // 0 TO 100, CLAMPED; MUTE NEVER CHANGES IT
player.mute(true);
player.quality(MediaQuality.HIGH);                // THE CLOSEST AVAILABLE; HOT SWITCH ON VIDEO, NEXT START ON IMAGES
player.scale(1280, 720);                          // UPLOAD SIZE CAP PER AXIS, 0 FOR NO CAP
player.lod(MediaPlayer.LodLevel.FAR);             // UPLOAD SIZE BY DISTANCE: MAX 100%, CLOSE 75%, NEAR 50%, FAR 25%, FAR_AWAY 10%
player.displayTime(10_000);                       // A STILL IMAGE ENDS AFTER THIS AND NEVER LOOPS; 0 KEEPS IT UNTIL STOPPED
player.audioLanguage("es-MX");                    // PREFERRED AUDIO LANGUAGE FROM THE NEXT START

// WHAT THE MEDIA ALLOWS: ASK BEFORE SHOWING A CONTROL
boolean playable = player.canPlay();              // FROM THE FIRST SUCCESSFUL LOAD (FROM CREATION ON A CLOCK) UNTIL IT FAILS OR IS RELEASED
boolean seekable = player.canSeek();              // false UNTIL THE SOURCE OPENS, ON LIVE STREAMS AND ON STILL IMAGES
boolean speedable = player.canSpeed();            // A PURE QUERY, SAFE FROM ANY THREAD ON EVERY FRAME
boolean live = player.live();                     // false UNTIL THE SOURCE OPENS
boolean positional = player.spatial();            // POSITIONAL AUDIO IS AVAILABLE

// WHERE IT IS
MediaPlayer.Status status = player.status();      // ALSO playing(), paused(), buffering(), ended(), error()...
long time = player.time();                        // 0 BEFORE THE FIRST FRAME, THE DURATION ONCE ENDED
long duration = player.duration();                // 0 WHEN UNKNOWN OR LIVE
int buffered = player.buffered();                 // 0 TO 100: REAL PROGRESS WHILE A VIDEO LOADS OR BUFFERS; IMAGES AND CLOCKS REPORT 100 OR 0
float fps = player.fps();                         // 0 UNTIL KNOWN
Throwable failure = player.exception();           // WHY IT LANDED IN ERROR
int width = player.width();                       // UPLOADED SIZE; sourceWidth() AND sourceHeight() GIVE THE NATIVE ONE
int height = player.height();

// DRAW: ON THE RENDER THREAD, READ FRESH ON EVERY FRAME
long texture = player.texture();                  // OPENGL TEXTURE NAME OR VULKAN VkImageView HANDLE; 0 MEANS NOTHING TO DRAW

// CLEAN UP WHEN THE HOST DROPS THE SURFACE, FAILED PLAYBACK INCLUDED
player.release();                                 // IDEMPOTENT, LEAVES status() AT STOPPED; NEVER FROM A LISTENER OR PLAYBACK THREAD

Вызывайте speed(...), когда аудиоконтекст текущий: часы берут только ту скорость, которую принял аудиодвижок. Встроенные графические движки пока не принимают сжатые текстуры BC.

final MediaPlayer.StatusListener playlist = new MediaPlayer.StatusListener() {
    @Override
    public void onStatus(final MediaPlayer source, final MediaPlayer.Status previous, final MediaPlayer.Status current) {
        // EVERY REAL TRANSITION, ONE AT A TIME AND IN ORDER; KEEP IT SHORT AND NEVER BLOCK
        if (current != MediaPlayer.Status.ENDED) return;
        source.onStatus(this); // ENDED, STOPPED AND ERROR CONSUME THE LISTENERS: REGISTER AGAIN TO KEEP HEARING
        source.start();        // RESTARTING HERE IS FINE; source.release() NEVER IS
    }
};
player.onStatus(playlist); // ADDING IT TWICE KEEPS IT ONCE
  • Слушатель выполняется в том потоке, который вызвал изменение: в потоке воспроизведения, в тикере синхронизации или, у изображений, в потоке того, кто вызывает status(), time() или texture().
  • Он никогда не выполняется, пока удерживается блокировка плеера, и не выполняется в потоке, которого ждёт release(). STOPPED от release() приходит, когда всё уже освобождено, включая движки.
Состояние плеера Значение
WAITING Создан и ждёт условий для запуска.
LOADING Загружает или подготавливает медиа.
BUFFERING Ждёт достаточного количества данных, чтобы продолжить.
PLAYING Воспроизводит медиа.
PAUSED На паузе и готов продолжить.
STOPPED Остановлен и готов начать с начала.
ENDED Дошёл до конца медиа.
ERROR Воспроизведение столкнулось с ошибкой.

Status.allows(next) сообщает, может ли пауза, продолжение, буферизация, завершение, остановка или ошибка перевести статус прямо в next. Запуск, перемотка и перезапуск следуют своим правилам: LOADING заканчивается, когда открывается пайплайн (или при остановке или ошибке), перемотка в ENDED или STOPPED перезапускает плеер, а у STOPPED нет прямых переходов.

3.2 АУДИОДОРОЖКИ И ЯЗЫКИ

Дублированные видео и файлы с несколькими аудиопотоками воспроизводят оригинальный звук (или звук файла по умолчанию), если вы не попросите другой.

player.audioLanguage("es-MX"); // PREFERRED FROM THE NEXT START; null CLEARS IT
for (final MediaPlayer.AudioTrack track: player.audioTracks()) {
    if ("es".equals(track.lang())) player.audioTrack(track); // SWITCHES RIGHT AWAY ON A RUNNING PLAYER
}
  • Порядок выбора такой: то, что вы выбрали через audioTrack(...), затем audioLanguage(...), затем оригинал (или аудиопоток файла по умолчанию), а иначе первый поток. es-MX сначала ищет es-MX, затем es-419, es или spa. Без audioLanguage(...) играет оригинал, а не автоматический дубляж.
  • audioTracks() перечисляет по одному AudioTrack(track, stream, name, lang) на каждый поток: сначала потоки основного файла, затем каждой отдельной дорожки. track равен AudioTrack.MAIN или индексу отдельной дорожки, а stream является индексом FFmpeg внутри неё.
  • Отдельная дорожка, которая ещё не воспроизводилась, показывается как одна запись provisional() (stream равен -1) с названием и языком от платформы, поэтому получение списка не открывает все дубляжи. Как только она заиграет, её заменят реальные потоки. Список неизменяемый, и его может читать любой поток.
  • audioTrack(track) возвращает true, если выбор изменился. Работающий плеер перестраивает свой пайплайн на текущем времени (прямой эфир перескакивает к самому свежему моменту), а выбор сохраняется при смене качества и перезапусках. audioTrack() возвращает то, что играет сейчас. Плееры изображений, серверные плееры и плееры без звука ничего не перечисляют.

3.3 ВЫБОР ДВИЖКОВ

Vulkan идёт в первую очередь, а OpenGL остаётся поддерживаемым. Какой бэкенд использовать, решает ваш слой рендеринга.

Фабрика Обязательства хоста
PlayerAPI.glEngine(renderThread, executor) Исполнитель работает в потоке контекста OpenGL: выполняйте его задачи каждый кадр и ещё раз после release(). Если оба аргумента null, контекстом должен владеть вызывающий поток. Никогда не переносите движок между контекстами.
PlayerAPI.vkEngine(context) Передайте VKContext, который живёт дольше движка: держите queueLock() вокруг своих submit и present и выполняйте колбэки retire(...), когда завершатся ваши кадры в полёте (все сразу, если устройство уходит). Чтобы декодировать на GPU, см. Декодирование через Vulkan.
PlayerAPI.awtEngine(onFrame) Рисуйте опубликованное изображение из UI на AWT/Swing; при необходимости перенаправляйте необязательный колбэк в поток UI.
PlayerAPI.jfxEngine(onFrame) Нужна среда выполнения JavaFX 13+; привяжите изображение движка к своему UI. Без неё фабрика бросает IllegalStateException вместо ошибки линковки.
PlayerAPI.headlessEngine() / headlessEngine(preload) Захватывает кадры без графического контекста, удобно для тестов и обработки изображений. Как и любой движок, существует только на клиентах; выделенный сервер создаёт только часы ServerMediaPlayer.
PlayerAPI.alEngine() Контекст OpenAL должен быть текущим в потоках, которые работают со звуком.
PlayerAPI.alEngine(true) Тот же контракт OpenAL, воспроизведение через позиционные моно-синки (стереопары и дополнительные динамики).
PlayerAPI.jsEngine() Устройство и линия Java Sound доступны; без позиционного звука.

Important

OpenGL: для конвертации YUV и видео в оттенках серого на GPU нужен OpenGL 3.2 core (GLSL 150); без него кадры конвертируются в BGRA на CPU. Для загрузки прямо из потока декодирования нужен OpenGL 4.4 или ARB_buffer_storage; более старые драйверы загружают кадры в потоке рендеринга через два пиксельных буфера.

Important

Vulkan: queue() должна принимать задачи compute и transfer.

  • Zero copy: декодированные плоскости попадают на GPU без копирования на CPU, если устройство включает VK_EXT_external_memory_host (hostImportSupported() возвращает true) и буфер плоскости выровнен по minImportedHostPointerAlignment(); иначе плоскость копируется через staging-буфер.
  • Аппаратная конвертация YUV: 8-битные SDR-кадры NV12 и YUV 4:2:0, 4:2:2 и 4:4:4 с чётными размерами и без основных цветов BT.2020 конвертируются аппаратно в сэмплере, если устройство включает samplerYcbcrConversion (Vulkan 1.1, ycbcrSampler() возвращает true); все остальные раскладки используют вычислительный проход (compute pass).
  • Раскладки: помимо 8-битных BGRA, RGBA, YUYV и UYVY, раскладке нужны форматы плоскостей с поддержкой сэмплирования, линейной фильтрации и записи через transfer, иначе она откатывается на BGRA. RGB откатывается всегда.
  • Декодирование на GPU: см. Декодирование через Vulkan.

Движки запечатаны (sealed): вы предоставляете контексты и исполнители, а не собственные движки. Каждый движок сам управляет своими текстурами, аудиоисточниками и буферами.

Все движки следуют одному и тому же жизненному циклу:

  • release() идемпотентен, никогда не должен выполняться одновременно с плеером, который передаёт ему данные, и оставляет движок неактивным: released() равен true, а texture() возвращает 0.
  • Плеер проверяет supports(format, bits) перед загрузкой и откатывается на 8-битный BGRA, если движок отказывается от раскладки.
  • Загрузки никогда не бросают исключений: при сбое драйвера кадр отбрасывается с одной ошибкой в логе, а последний кадр остаётся на экране.
  • Программные и headless-движки отдают ненулевое значение-заглушку (sentinel) вместо дескриптора GPU, поэтому сравнивайте дескриптор только с 0.
  • PixelFormat.planeWidth, planeHeight, texelBytes и frameBytes вычисляют размер каждой плоскости движка, например, чтобы узнать, сколько памяти он использует. Они округляют вверх, как FFmpeg, поэтому нечётные размеры не теряют цветность.

Работа с цветом:

  • Движки OpenGL и Vulkan конвертируют YUV с той матрицей (BT.601, BT.709 или BT.2020), диапазоном и положением цветности, которые объявляет каждое видео. Если видео ничего не объявляет, они угадывают, как mpv: BT.601 для SD-кадров (уже 1280 пикселей и высотой до 576 строк), иначе BT.709.
  • HDR в PQ и HLG отображается как SDR: цвета BT.2020 переводятся в BT.709, а светлые участки сжимаются так, чтобы пик в 1000 нит достигал белого.
  • Движки на одном контексте или устройстве делят эту конвертацию, и она уходит вместе с последним из них. Освободите каждый движок и дайте его очистке выполниться (в исполнителе OpenGL или в VKContext.retire), прежде чем уничтожать контекст или устройство.
  • Программные движки получают BGRA, который FFmpeg конвертирует с объявленными матрицей и диапазоном, без tone mapping для HDR.

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

Important

OpenAL: 4-, 6-, 7- и 8-канальному звуку на обычном движке нужен AL_EXT_MCFORMATS (пространственные движки сводят всё в стерео), моно- и стереосэмплам float и double нужны AL_EXT_FLOAT32 и AL_EXT_DOUBLE (surround во float идёт вместе с AL_EXT_MCFORMATS), а часам, которые учитывают задержку вывода устройства, нужен AL_SOFT_source_latency; без них звук конвертируется в то, что устройство умеет воспроизводить. Обработчикам окружения, которые используют EFX, нужен ALC_EXT_EFX.

Автономное приложение (при открытии JAR WATERMeDIA) использует Vulkan, если вы не выберете OpenGL, а если Vulkan недоступен, именно этот запуск идёт на OpenGL, а ваш выбор не меняется. -Dwatermedia.engine=opengl или -Dwatermedia.engine=vulkan принудительно задаёт движок на один запуск. Его настройки (движок, масштаб интерфейса, CRT-эффект и звук клика) сохраняются в config/app.properties.

Декодирование через Vulkan

По умолчанию VKContext.deviceInfo() возвращает null, и видео загружается из RAM. Переопределите его, описав, как вы создали своё устройство (цепочка возможностей, переданная в vkCreateDevice, которую нельзя освобождать или менять, пока живёт устройство, включённые расширения экземпляра и устройства и очереди, созданные для каждого семейства), и FFmpeg будет декодировать H.264, H.265 и AV1 через Vulkan Video без пересылки через RAM.

Important

Для декодирования через Vulkan нужны:

  • Экземпляр и устройство Vulkan 1.3 с включёнными timelineSemaphore и synchronization2.
  • VK_KHR_video_queue, VK_KHR_video_decode_queue и расширение декодирования для каждого ускоряемого кодека: VK_KHR_video_decode_h264, VK_KHR_video_decode_h265 или VK_KHR_video_decode_av1.
  • Свободные очереди на вашем устройстве или расширения внешней памяти и семафоров для двойника; оба варианта описаны ниже.
  • Нативные библиотеки с Vulkan Video (CodecsAPI.vulkanDecode() возвращает true) и включённый codecs.ffmpeg.hardwareAccel.

Тогда FFmpeg декодирует в одном из двух мест:

  • Ваше устройство, если у него есть свободные очереди для FFmpeg: очередь compute (она же принимает transfer), отличная от queue(), и очередь декодирования видео, причём queue() создана последней в queueFamily(). FFmpeg забирает все остальные очереди каждого семейства и отправляет в них работу из своих потоков, поэтому, пока существуют плееры, не трогайте эти очереди и не вызывайте vkDeviceWaitIdle; ожидайте собственную очередь (vkQueueWaitIdle под queueLock()).
  • Двойник, если у вашего устройства нет таких очередей, но включены VK_KHR_external_memory_win32 и VK_KHR_external_semaphore_win32 на Windows или VK_KHR_external_memory_fd и VK_KHR_external_semaphore_fd на других системах. Движок создаёт второе устройство на том же GPU, FFmpeg декодирует там, и каждое изображение импортируется в ваше устройство. Ваши очереди остаются только вашими.

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

Important

Видео в 4:2:2, 4:4:4 и 12 бит декодируется на GPU только через libplacebo, которая превращает любую декодированную раскладку в SDR RGBA (HDR проходит tone mapping). Ей нужны hostQueryReset, runtimeDescriptorArray, shaderStorageImageArrayNonUniformIndexing и maintenance4 на декодирующем устройстве, а на вашем собственном устройстве ещё и вторая очередь в queueFamily(); двойник включает эти возможности сам, если GPU их поддерживает. Без libplacebo на GPU декодируются только потоки 4:2:0 (8 и 10 бит), и движок конвертирует их своими шейдерами.

3.4 ПРОСТРАНСТВЕННЫЙ ЗВУК

Создайте движок через PlayerAPI.alEngine(true) до создания плеера; пространственный режим остаётся зафиксированным на всё время его жизни. Стерео- и surround-звук сводится в стерео (моно остаётся моно) и воспроизводится через синки (sinks), то есть позиционные моно-источники. Обычный alEngine() воспроизводит всё на одном источнике без позиции, а у Java Sound пространственного звука нет (он возвращает false).

Движок стартует с одним основным синком на Mix.MONO, без позиции. Передайте SpatialAudio, чтобы разместить его в мире, или null, чтобы вернуться к обычному воспроизведению без позиции и эффектов. Громкость, отключение звука, скорость, пауза и sync доходят до каждого синка.

import org.watermedia.api.player.SFXEngine.SpatialAudio;

final ALEngine audio = PlayerAPI.alEngine(true);
final SpatialAudio position = new SpatialAudio(
        blockX + 0.5, blockY + 0.5, blockZ + 0.5,
        8.0f, 64.0f, 1.0f, false, null);
audio.spatialAudio(position);

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

  • FFMediaPlayer использует собственные потоки, поэтому контекст должен быть текущим для всего процесса. Хосты, которые делают его текущим только для отдельного потока (thread-local), не поддерживаются.
  • Движок отклоняет чужой или отсутствующий контекст и никогда не переключает и не пересоздаёт ваш.
  • Освобождайте плееры перед перезагрузкой устройства, а затем создавайте новые движки в новом контексте.
  • FFMediaPlayer.release() останавливает свой пайплайн и дожидается его завершения перед удалением источника: вызывайте его до уничтожения контекста и никогда из колбэка этого пайплайна.

SpatialAudio отклоняет позиции, которые не помещаются во float OpenAL, расстояния, не удовлетворяющие 0 < referenceDistance < maxDistance, и отрицательный rolloff (0 отключает затухание). То, как звук затухает, решает модель расстояния вашего хоста; API никогда не трогает ни её, ни слушателя, ни скорость источника, поэтому maxDistance не всегда означает тишину.

Синки: стереопары и дополнительные динамики

Одно декодирование обслуживает все синки, поэтому стереопара или дополнительный динамик не стоят ни второго скачивания, ни второго декодирования. Каждый синк воспроизводит один ALEngine.Mix: LEFT, RIGHT или MONO (среднее). На моно-медиа все миксы звучат одинаково, поэтому стереопара никогда не замолкает.

// A STEREO PICTURE: THE PRIMARY TURNS INTO THE LEFT EDGE AND A NEW SINK PLAYS THE RIGHT ONE
final ALEngine.Sink left = player.sinks().get(0);
left.mix(ALEngine.Mix.LEFT);
final ALEngine.Sink right = player.addSink(ALEngine.Mix.RIGHT);
left.spatialAudio(new SpatialAudio(leftX, y, z, 8.0f, 64.0f, 1.0f, false, environment));
right.spatialAudio(new SpatialAudio(rightX, y, z, 8.0f, 64.0f, 1.0f, false, environment));

// A SPEAKER BLOCK: ONE MORE SINK AT ITS OWN POSITION, RELEASED WHEN THE BLOCK GOES AWAY
final ALEngine.Sink speaker = player.addSink(ALEngine.Mix.MONO);
speaker.spatialAudio(new SpatialAudio(speakerX, speakerY, speakerZ, 8.0f, 64.0f, 1.0f, false, environment));
speaker.release();
  • Основной синк владеет player.audioSource(), ведёт часы и живёт столько же, сколько движок (его release() возвращает false). player.sinks() перечисляет основной первым; без пространственного движка OpenAL sinks() даёт пустой список, а addSink(...) даёт null.
  • Каждый синк ставит в очередь одни и те же сэмплы, и управляются они вместе, поэтому стереопара остаётся выровненной с точностью до сэмпла. Добавление синка во время воспроизведения или смена микса на стерео-медиа заново выравнивает очереди по основному синку с крошечной паузой.
  • У каждого синка свой spatialAudio(...): адаптер окружения получает источник этого синка, а sink.release() сбрасывает его перед удалением источника. Освобождение плеера освобождает все синки.
  • Добавляйте, изменяйте и освобождайте синки в звуковом исполнителе, когда контекст текущий; их запросам (source(), mix(), spatialAudio(), released()) он не нужен.
  • Хост может использовать источник синка в своей звуковой системе и менять его усиление (gain), высоту тона (pitch) и позицию, но никогда не запускать, не ставить в очередь, не снимать с очереди и не удалять его. Новый синк стартует с усилением и высотой тона движка.

Обработчики окружения

Последний аргумент SpatialAudio представляет собой необязательный SpatialAudio.Environment, который применяет ваши собственные эффекты, например окклюзию или реверберацию, к каждому источнику OpenAL. Это дверь для модов пространственного звука: напишите в своём моде небольшой адаптер, который передаёт им каждый источник, и загружайте его, только когда такой мод установлен.

final SpatialAudio.Environment environment = new SpatialAudio.Environment() {
    @Override
    public void process(final int source, final SpatialAudio audio) {
        // EVERY spatialAudio(...) UPDATE: APPLY YOUR WHOLE ENVIRONMENT (EFX FILTERS AND SENDS) TO source
    }

    @Override
    public void reset(final int source) {
        // THE PROCESSOR IS REPLACED, REMOVED OR THE SINK IS RELEASED: REMOVE YOUR EFFECTS FROM source
    }
};
player.spatialAudio(new SpatialAudio(x, y, z, 8.0f, 64.0f, 1.0f, false, environment));
  • Держите один стабильный экземпляр обработчика между обновлениями и вызывайте spatialAudio(...) из звукового исполнителя хоста, когда контекст текущий.
  • Перед каждым process позиция восстанавливается, а фильтры и посылы (sends) источника очищаются, поэтому вы можете сдвигать AL_POSITION, но должны применять всё окружение при каждом вызове.
  • Ограничивайте частоту обновлений на стороне хоста (а не досрочным выходом внутри обработчика) и распределяйте много плееров по разным тикам.
  • Движок удаляет только свой источник и буферы, но никогда фильтры или слоты эффектов обработчика.
  • Если process или reset бросает исключение, эффекты очищаются, а исключение идёт дальше; падающий reset не мешает удалить источник при освобождении.
  • auxOnly = true просит выдавать только отражённый звук, но не глушит прямой путь: отключайте звук плеера, когда нужна полная тишина.
  • Обработчик выполняется только при ваших обновлениях и никогда не из цикла декодирования. Базовому позиционированию EFX не нужен; обработчику, который использует EFX, нужно устройство с его поддержкой.

Проверка

SpatialAudioTest и ALSinkTest проверяют пространственный звук и синки на реальном OpenAL: аргументы, затухание, окружения, исключения, контексты, миксы и опустошение буфера (underrun). Им нужно устройство вывода OpenAL, а случаям с декодированием ещё и нативные библиотеки FFmpeg.

3.5 СИНХРОННОЕ ВОСПРОИЗВЕДЕНИЕ

PlayerAPI.serverPlayer() создаёт ServerMediaPlayer: часы без медиа и движков, которые работают на обеих сторонах. Они следят за временем (запуск, пауза, перемотка, скорость, повтор и конец), а каждое изменение увеличивает revision(). Остальное за вами: кто управляет часами, как снимки доходят до клиентов и где вы сохраняете позицию.

ServerMediaPlayer clock = PlayerAPI.serverPlayer();
clock.syncDuration(durationMs);
clock.start();
// ON EVERY SERVER TICK: SEND clock.snapshot() WITH YOUR OWN PACKET WHEN clock.revision() MOVED
MediaPlayer player = PlayerAPI.player(mrl, gfxSupplier, sfxSupplier);
player.sync(snapshot); // ON THE CLIENT, FOR EVERY SNAPSHOT RECEIVED

Плеер, получивший снимок через sync(PlayerState) (или sync(ByteBuffer) для закодированной формы), с этого момента следует за часами. Каждые 50 мс он:

  • Повторяет статус снимка: запуск, паузу и остановку. Снимок с BUFFERING, который бывает только у собранного вручную снимка, удерживается как пауза.
  • Перенимает его повтор и скорость.
  • Корректирует позицию через seekQuick (с ограничением частоты), когда drift() превышает tolerance(ms) (по умолчанию одна секунда).

Последний полученный снимок всегда побеждает, поэтому отправляйте их по порядку. authority() возвращает этот снимок, authorityTime() оценивает, где часы находятся сейчас, а following() сообщает, следует ли плеер за часами. Коррекция никогда не трогает скорость, пропускается во время загрузки или буферизации, а на повторяющемся медиа идёт кратчайшим путём через цикл.

Команда, отданная плееру, который следует за часами, отменяется на следующем тике, поэтому передавайте команды пользователя часам своими пакетами, а результат пусть принесёт снимок. Покадровый шаг отклоняется, а громкость, отключение звука, масштабирование и пространственный звук всегда остаются локальными.

Часы держат свою позицию (ноль после start()), пока syncDuration(ms) или live(true) не скажет им, что играет. Отправьте длительность один раз с клиента, который открыл источник (побеждает первое положительное значение), и восстанавливайте сохранённую позицию через syncTime(ms) после start(), который начинает с нуля. Без длительности или в прямом эфире перемотки нет (canSeek() равен false). release() замораживает позицию и оставляет часы в STOPPED: отправьте этот последний снимок, прежде чем избавляться от часов, иначе у клиентов останется предыдущий. Освобождайте каждый плеер, когда заканчивается его сессия.

Формат снимка

MediaPlayer.PlayerState представляет собой запись фиксированного размера в 27 байт в порядке big-endian (версия, ревизия, статус, время, длительность, скорость, флаги). PlayerState.read(ByteBuffer) читает только эти байты при любом порядке байтов буфера и оставляет остальное вашему транспорту; если снимок отклонён, позиция буфера не сдвигается. Сами проверяйте, кто отправил каждый снимок: библиотека никого не идентифицирует.

Clone this wiki locally