Подробное описание реализации HDR10, HLG и Dolby Vision: docs/HDR_MODE.md
DDD-VR — Android/OpenXR видеоплеер для запуска видео в VR-режиме.
Проект построен вокруг OpenXR-активити, Media3/ExoPlayer и native OpenXR-рендера. Основной сценарий — открыть видео из внешнего приложения, лаунчера, Lampa/DDDPlayer2-интеграции или через adb, передав ссылку на видео через Android Intent.
Пакет приложения:
top.rootu.dddvr
Основная activity:
top.rootu.dddvr.xr.activity.OpenXrPlayerActivity
DDD-VR получает входной URI через Android Intent, создаёт playback request, передаёт видео в Media3/ExoPlayer, получает Surface от native OpenXR-слоя и выводит видеокадры в VR-сцену.
Общая схема:
External app / ADB / Launcher
↓
Intent.ACTION_VIEW
↓
OpenXrPlayerActivity
↓
VrIntentParser
↓
PlayerManager / PlaybackSession
↓
Media3 ExoPlayer
↓
Surface from OpenXR native layer
↓
OpenXR VR rendering
Основная OpenXR-активити сохраняет последний переданный URI. Если приложение открывается без нового URI, оно пробует восстановить последний запуск.
DDD-VR сейчас умеет:
-
запускаться как обычное Android-приложение;
-
запускаться как Leanback/TV-приложение;
-
принимать видео через
Intent.ACTION_VIEW; -
принимать URI через
intent.data; -
принимать URI через
Intent.EXTRA_STREAM; -
запускать воспроизведение с заданной позиции;
-
определять или принимать явно тип входного стерео-видео;
-
передавать видео в Media3/ExoPlayer;
-
подключать ExoPlayer к Surface, созданному OpenXR-слоем;
-
выводить видео через native OpenXR bridge;
-
управлять воспроизведением из OpenXR input:
- play/pause;
- seek назад;
- seek вперёд;
- seek по таймлайну;
- recenter;
- show/hide menu;
- exit;
-
передавать состояние UI в native OpenXR-слой:
- видимость меню;
- прогресс воспроизведения;
- состояние play/pause;
-
передавать размер видео в native OpenXR-слой;
-
сохранять последний URI и заголовок видео.
OpenXrPlayerActivity объявлена как обработчик для следующих схем:
http
https
ftp
rtsp
content
file
magnet
acestream
Примеры входных URI:
https://example.com/video.mp4
http://192.168.1.10:8090/stream/video.m3u8
rtsp://example.com/live
content://media/external/video/media/123
file:///sdcard/Movies/video.mp4
magnet:?xt=urn:btih:...
acestream://...
DDD-VR принимает URI как входной адрес и передаёт его дальше в playback pipeline. Для обычных HTTP/HTTPS/RTSP/file/content-ссылок воспроизведение идёт через Media3/ExoPlayer.
Название видео.
intent.putExtra(Intent.EXTRA_TITLE, "Movie title")Начальная позиция воспроизведения в миллисекундах.
intent.putExtra("start_position_ms", 60_000L)Альтернативный ключ для начальной позиции. Значение также трактуется как миллисекунды.
intent.putExtra("position", 60_000L)Режим входного стерео-видео.
Поддерживаемые значения:
sbs
sbs_reversed
ou
ou_reversed
Пример:
intent.putExtra("stereo_mode", "sbs")Явное указание раскладки видео.
Поддерживаемые значения:
mono
sbs
ou
Пример:
intent.putExtra("stereo_layout", "ou")Тип упаковки стерео-видео.
Поддерживаемые значения:
full
half
Пример:
intent.putExtra("stereo_packing", "full")Тип проекции входного видео.
Поддерживаемые значения:
flat
curved
equirect_180
equirect_360
Пример:
intent.putExtra("projection", "flat")VR-режим экрана.
Поддерживаемые значения:
flat_vr_screen
curved
vr180
vr360
Пример:
intent.putExtra("vr_projection", "flat_vr_screen")Меняет левый и правый глаз местами.
intent.putExtra("swap_eyes", true)adb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video.mp4"adb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video.mp4" \
--es android.intent.extra.TITLE "Movie title"adb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video.mp4" \
--el start_position_ms 60000adb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video_sbs.mp4" \
--es stereo_mode sbsadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video_ou.mp4" \
--es stereo_mode ouadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video_sbs.mp4" \
--es stereo_mode sbs_reversedadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video_ou.mp4" \
--es stereo_mode ou_reversedadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video_sbs.mp4" \
--es stereo_mode sbs \
--ez swap_eyes trueadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video.mp4" \
--es vr_projection flat_vr_screenadb shell am start \
-n top.rootu.dddvr/.xr.activity.OpenXrPlayerActivity \
-a android.intent.action.VIEW \
-d "https://example.com/video.mp4" \
--es vr_projection curvedМинимальный Kotlin-пример:
val intent = Intent(Intent.ACTION_VIEW).apply {
setClassName(
"top.rootu.dddvr",
"top.rootu.dddvr.xr.activity.OpenXrPlayerActivity"
)
setDataAndType(
Uri.parse("https://example.com/video.mp4"),
"video/*"
)
putExtra(Intent.EXTRA_TITLE, "Movie title")
putExtra("start_position_ms", 0L)
putExtra("stereo_mode", "sbs")
putExtra("vr_projection", "flat_vr_screen")
putExtra("swap_eyes", false)
}
startActivity(intent)Пример для OU-видео:
val intent = Intent(Intent.ACTION_VIEW).apply {
setClassName(
"top.rootu.dddvr",
"top.rootu.dddvr.xr.activity.OpenXrPlayerActivity"
)
data = Uri.parse("https://example.com/video_ou.mp4")
putExtra(Intent.EXTRA_TITLE, "OU video")
putExtra("stereo_mode", "ou")
putExtra("stereo_packing", "full")
}
startActivity(intent)Пример запуска с восстановлением позиции:
val intent = Intent(Intent.ACTION_VIEW).apply {
setClassName(
"top.rootu.dddvr",
"top.rootu.dddvr.xr.activity.OpenXrPlayerActivity"
)
data = Uri.parse("https://example.com/video.mp4")
putExtra(Intent.EXTRA_TITLE, "Movie title")
putExtra("start_position_ms", 125_000L)
}
startActivity(intent)Если stereo_mode не передан явно, DDD-VR пытается определить тип входного видео по URI через внутренний StereoTypeDetector.
Результат автоопределения маппится в один из режимов:
MONO
SBS
OU
Для внешней интеграции надёжнее передавать режим явно:
putExtra("stereo_mode", "sbs")или:
putExtra("stereo_mode", "ou")Native OpenXR-слой передаёт в Kotlin-часть действия управления. Они обрабатываются в OpenXrBridge и OpenXrPlayerActivity.
Сейчас используются следующие действия:
| Действие | Поведение |
|---|---|
PLAY_PAUSE |
Переключает play/pause |
SEEK_BACK |
Перемотка на 15 секунд назад |
SEEK_FORWARD |
Перемотка на 15 секунд вперёд |
RECENTER |
Запрос recenter |
SHOW_MENU |
Показать или скрыть OpenXR UI |
EXIT |
Закрыть плеер |
| timeline seek | Перемотка по прогрессу от 0 до 1000 |
Воспроизведение строится на Media3/ExoPlayer.
Внутри OpenXrPlayerActivity создаётся один MediaItem:
MediaItem(
uri = request.uri,
title = request.title,
startPositionMs = request.startPositionMs
)Дальше он передаётся в PlayerManager.loadPlaylist(...).
Когда native OpenXR-слой создаёт Surface, activity получает его через callback и подключает к PlaybackSession:
playbackSession.attachSurface(surface)При изменении видеоформата ширина и высота видео передаются обратно в OpenXR bridge:
bridge.setVideoSize(width, height)Состояние интерфейса передаётся в native-слой через:
bridge.setUiState(
visible = openXrUiVisible,
progressPermille = progress,
playing = playing
)При запуске с новым URI DDD-VR сохраняет:
last_uri
last_title
Хранилище:
SharedPreferences: openxr_player
Если приложение открыто без URI, оно пробует восстановить последний сохранённый URI и запустить его повторно.
Требования для сборки:
JDK 17
Android SDK Platform 36
Android Build Tools 36.0.0
CMake 3.22.1
Android NDK 27.0.12077973
Сборка debug APK:
./gradlew assembleDebugЗапуск тестов:
./gradlew testAPK после сборки:
app/build/outputs/apk/debug/
Установка на устройство:
adb install -r app/build/outputs/apk/debug/app-debug.apkВ репозитории настроена автоматическая сборка Android APK.
Workflow делает:
checkout
setup JDK 17
setup Android SDK
install platform-tools / android-36 / build-tools / cmake / ndk
./gradlew assembleDebug
проверку libdddvr_openxr.so внутри APK
./gradlew test
upload debug APK artifact
Имя artifact:
dddvr-debug-apk
Основные компоненты:
Kotlin
AndroidX
Media3 / ExoPlayer
OpenXR
CMake
C++17
Room
OkHttp
Gson
Coil
Leanback
Параметры Android-сборки:
namespace: top.rootu.dddvr
applicationId: top.rootu.dddvr
minSdk: 23
targetSdk: 34
compileSdk: 36
Native library:
libdddvr_openxr.so
Основной Android-код:
app/src/main/java/top/rootu/dddvr/
Ключевые части:
xr/activity/OpenXrPlayerActivity.kt
xr/bridge/OpenXrBridge.kt
xr/model/OpenXrPlaybackConfig.kt
vr/activity/VrIntentParser.kt
vr/activity/VrPlaybackRequest.kt
player/PlayerManager.kt
core/playback/PlaybackSession.kt
model/MediaItem.kt
Native/OpenXR-код:
app/src/main/cpp/
Сборочный файл native-части:
app/src/main/cpp/CMakeLists.txt
Manifest:
app/src/main/AndroidManifest.xml
Для интеграции с Lampa или DDDPlayer2 внешняя сторона должна открыть DDD-VR через explicit intent и передать URI видео.
Базовый контракт:
package: top.rootu.dddvr
activity: top.rootu.dddvr.xr.activity.OpenXrPlayerActivity
action: android.intent.action.VIEW
data: video URI
Рекомендуемые extras:
Intent.EXTRA_TITLE
start_position_ms
stereo_mode
vr_projection
swap_eyes
Пример:
val intent = Intent(Intent.ACTION_VIEW).apply {
setClassName(
"top.rootu.dddvr",
"top.rootu.dddvr.xr.activity.OpenXrPlayerActivity"
)
data = Uri.parse(videoUrl)
putExtra(Intent.EXTRA_TITLE, title)
putExtra("start_position_ms", positionMs)
putExtra("stereo_mode", "sbs")
putExtra("vr_projection", "flat_vr_screen")
putExtra("swap_eyes", false)
}
context.startActivity(intent)Проект распространяется под лицензией GPL-3.0.
См. файл:
LICENSE