Repository navigation
RU RU 3 Playing media
Note
Настраивается: player.defaultQuality, player.cacheBudget, player.watertx.cache и
player.watertx.texturesBudget. Смотрите их комментарии в config/watermedia.toml.
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 нет прямых
переходов.
Дублированные видео и файлы с несколькими аудиопотоками воспроизводят оригинальный звук (или звук файла по умолчанию), если вы не попросите другой.
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()возвращает то, что играет сейчас. Плееры изображений, серверные плееры и плееры без звука ничего не перечисляют.
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.
По умолчанию 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 бит), и движок конвертирует их своими шейдерами.
Создайте движок через 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()перечисляет основной первым; без пространственного движка OpenALsinks()даёт пустой список, а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.
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) читает только эти байты при любом
порядке байтов буфера и оставляет остальное вашему транспорту; если снимок отклонён, позиция буфера не сдвигается. Сами
проверяйте, кто отправил каждый снимок: библиотека никого не идентифицирует.