NuvioStreaming/composeApp/src/desktopMain/native/windows/player_bridge.cpp
2026-06-09 19:55:57 +05:30

1965 lines
73 KiB
C++

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <dwmapi.h>
#include <wrl.h>
#include <WebView2.h>
#include <jni.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cwctype>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
using Microsoft::WRL::Callback;
using Microsoft::WRL::ComPtr;
extern "C" {
typedef struct mpv_handle mpv_handle;
typedef enum mpv_format {
MPV_FORMAT_NONE = 0,
MPV_FORMAT_STRING = 1,
MPV_FORMAT_OSD_STRING = 2,
MPV_FORMAT_FLAG = 3,
MPV_FORMAT_INT64 = 4,
MPV_FORMAT_DOUBLE = 5,
} mpv_format;
typedef enum mpv_event_id {
MPV_EVENT_NONE = 0,
MPV_EVENT_SHUTDOWN = 1,
} mpv_event_id;
typedef struct mpv_event {
mpv_event_id event_id;
int error;
uint64_t reply_userdata;
void *data;
} mpv_event;
}
namespace {
HMODULE gModule = nullptr;
constexpr UINT WM_NUVIO_TASK = WM_APP + 0x4E50;
constexpr UINT_PTR NUVIO_TIMER_ID = 0x4E50;
const wchar_t *kMessageWindowClass = L"NuvioPlayerBridgeMessageWindow";
const wchar_t *kContainerWindowClass = L"NuvioPlayerBridgeContainerWindow";
constexpr DWORD kDwmwaUseImmersiveDarkMode = 20;
constexpr DWORD kDwmwaUseImmersiveDarkModeLegacy = 19;
constexpr DWORD kDwmwaBorderColor = 34;
constexpr DWORD kDwmwaCaptionColor = 35;
constexpr DWORD kDwmwaTextColor = 36;
std::wstring toWide(const std::string &value) {
if (value.empty()) return std::wstring();
int size = MultiByteToWideChar(CP_UTF8, 0, value.data(), (int)value.size(), nullptr, 0);
if (size <= 0) return std::wstring();
std::wstring result((size_t)size, L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.data(), (int)value.size(), result.data(), size);
return result;
}
std::string toUtf8(const std::wstring &value) {
if (value.empty()) return std::string();
int size = WideCharToMultiByte(CP_UTF8, 0, value.data(), (int)value.size(), nullptr, 0, nullptr, nullptr);
if (size <= 0) return std::string();
std::string result((size_t)size, '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), (int)value.size(), result.data(), size, nullptr, nullptr);
return result;
}
std::string jstringToUtf8(JNIEnv *env, jstring value) {
if (!value) return std::string();
jsize length = env->GetStringLength(value);
const jchar *chars = env->GetStringChars(value, nullptr);
if (!chars) return std::string();
std::wstring wide(reinterpret_cast<const wchar_t *>(chars), (size_t)length);
env->ReleaseStringChars(value, chars);
return toUtf8(wide);
}
jstring newJavaStringUtf8(JNIEnv *env, const std::string &value) {
std::wstring wide = toWide(value);
return env->NewString(reinterpret_cast<const jchar *>(wide.data()), (jsize)wide.size());
}
std::vector<std::string> jstringArrayToVector(JNIEnv *env, jobjectArray values) {
std::vector<std::string> result;
if (!values) return result;
jsize count = env->GetArrayLength(values);
result.reserve((size_t)count);
for (jsize index = 0; index < count; index++) {
jstring item = (jstring)env->GetObjectArrayElement(values, index);
std::string value = jstringToUtf8(env, item);
if (!value.empty()) {
result.push_back(value);
}
env->DeleteLocalRef(item);
}
return result;
}
void throwJavaError(JNIEnv *env, const std::string &message) {
jclass exceptionClass = env->FindClass("java/lang/IllegalStateException");
if (exceptionClass) {
env->ThrowNew(exceptionClass, message.c_str());
}
}
std::string trim(const std::string &value) {
const char *spaces = " \t\r\n";
size_t start = value.find_first_not_of(spaces);
if (start == std::string::npos) return std::string();
size_t end = value.find_last_not_of(spaces);
return value.substr(start, end - start + 1);
}
std::string lowerCopy(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return (char)std::tolower(ch);
});
return value;
}
COLORREF rgbIntToColorRef(jint rgb) {
BYTE red = (BYTE)((rgb >> 16) & 0xFF);
BYTE green = (BYTE)((rgb >> 8) & 0xFF);
BYTE blue = (BYTE)(rgb & 0xFF);
return RGB(red, green, blue);
}
void setDwmWindowAttribute(HWND hwnd, DWORD attribute, const void *value, DWORD valueSize) {
(void)DwmSetWindowAttribute(hwnd, attribute, value, valueSize);
}
void applyDwmWindowChrome(HWND hwnd, bool darkMode, COLORREF captionColor, COLORREF borderColor, COLORREF textColor) {
if (!hwnd || !IsWindow(hwnd)) return;
BOOL enabled = darkMode ? TRUE : FALSE;
HRESULT darkModeResult = DwmSetWindowAttribute(
hwnd,
kDwmwaUseImmersiveDarkMode,
&enabled,
sizeof(enabled)
);
if (FAILED(darkModeResult)) {
setDwmWindowAttribute(hwnd, kDwmwaUseImmersiveDarkModeLegacy, &enabled, sizeof(enabled));
}
setDwmWindowAttribute(hwnd, kDwmwaCaptionColor, &captionColor, sizeof(captionColor));
setDwmWindowAttribute(hwnd, kDwmwaBorderColor, &borderColor, sizeof(borderColor));
setDwmWindowAttribute(hwnd, kDwmwaTextColor, &textColor, sizeof(textColor));
}
bool containsCaseInsensitive(const std::string &haystack, const std::string &needle) {
return lowerCopy(haystack).find(lowerCopy(needle)) != std::string::npos;
}
std::string jsonEscape(const std::string &value) {
std::string result;
result.reserve(value.size() + 8);
for (unsigned char ch : value) {
switch (ch) {
case '\\': result += "\\\\"; break;
case '"': result += "\\\""; break;
case '\b': result += "\\b"; break;
case '\f': result += "\\f"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if (ch < 0x20) {
char buffer[8];
std::snprintf(buffer, sizeof(buffer), "\\u%04x", ch);
result += buffer;
} else {
result.push_back((char)ch);
}
}
}
return result;
}
std::wstring javaScriptStringLiteral(const std::string &value) {
std::string escaped;
escaped.reserve(value.size() + 8);
escaped.push_back('"');
escaped += jsonEscape(value);
escaped.push_back('"');
return toWide(escaped);
}
std::wstring moduleDirectory() {
wchar_t buffer[MAX_PATH] = {};
DWORD length = GetModuleFileNameW(gModule, buffer, MAX_PATH);
if (length == 0 || length >= MAX_PATH) return std::wstring();
std::wstring path(buffer, buffer + length);
size_t separator = path.find_last_of(L"\\/");
if (separator == std::wstring::npos) return std::wstring();
return path.substr(0, separator);
}
std::wstring tempUserDataDirectory() {
wchar_t tempPath[MAX_PATH] = {};
DWORD length = GetTempPathW(MAX_PATH, tempPath);
std::wstring result = length > 0 ? std::wstring(tempPath, tempPath + length) : L".\\";
if (!result.empty() && result.back() != L'\\' && result.back() != L'/') {
result.push_back(L'\\');
}
result += L"NuvioWebView2";
CreateDirectoryW(result.c_str(), nullptr);
return result;
}
std::string hresultMessage(const char *operation, HRESULT hr) {
std::ostringstream builder;
builder << operation << " failed: 0x" << std::hex << (unsigned long)hr;
return builder.str();
}
struct MpvApi {
using mpv_create_fn = mpv_handle *(*)();
using mpv_initialize_fn = int (*)(mpv_handle *);
using mpv_terminate_destroy_fn = void (*)(mpv_handle *);
using mpv_set_option_fn = int (*)(mpv_handle *, const char *, mpv_format, void *);
using mpv_set_option_string_fn = int (*)(mpv_handle *, const char *, const char *);
using mpv_set_property_fn = int (*)(mpv_handle *, const char *, mpv_format, void *);
using mpv_set_property_string_fn = int (*)(mpv_handle *, const char *, const char *);
using mpv_get_property_fn = int (*)(mpv_handle *, const char *, mpv_format, void *);
using mpv_command_fn = int (*)(mpv_handle *, const char **);
using mpv_error_string_fn = const char *(*)(int);
using mpv_free_fn = void (*)(void *);
using mpv_wait_event_fn = mpv_event *(*)(mpv_handle *, double);
using mpv_wakeup_fn = void (*)(mpv_handle *);
HMODULE library = nullptr;
std::once_flag loadOnce;
std::string loadFailure;
mpv_create_fn create = nullptr;
mpv_initialize_fn initialize = nullptr;
mpv_terminate_destroy_fn terminateDestroy = nullptr;
mpv_set_option_fn setOption = nullptr;
mpv_set_option_string_fn setOptionString = nullptr;
mpv_set_property_fn setProperty = nullptr;
mpv_set_property_string_fn setPropertyString = nullptr;
mpv_get_property_fn getProperty = nullptr;
mpv_command_fn command = nullptr;
mpv_error_string_fn errorString = nullptr;
mpv_free_fn freeValue = nullptr;
mpv_wait_event_fn waitEvent = nullptr;
mpv_wakeup_fn wakeup = nullptr;
void ensureLoaded() {
std::call_once(loadOnce, [this]() { load(); });
if (!library) {
throw std::runtime_error(loadFailure.empty() ? "Unable to load libmpv-2.dll." : loadFailure);
}
}
std::string errorText(int error) {
if (!errorString) return "unknown";
const char *text = errorString(error);
return text ? text : "unknown";
}
void load() {
std::vector<std::wstring> candidates;
wchar_t envPath[32768] = {};
DWORD envCapacity = (DWORD)(sizeof(envPath) / sizeof(envPath[0]));
DWORD envLength = GetEnvironmentVariableW(L"NUVIO_LIBMPV_PATH", envPath, envCapacity);
if (envLength > 0 && envLength < envCapacity) {
candidates.emplace_back(envPath, envPath + envLength);
}
std::wstring moduleDir = moduleDirectory();
if (!moduleDir.empty()) {
candidates.push_back(moduleDir + L"\\libmpv-2.dll");
}
candidates.push_back(L"libmpv-2.dll");
candidates.push_back(L"C:\\Program Files (x86)\\Nuvio\\app\\native\\libmpv-2.dll");
candidates.push_back(L"C:\\msys64\\ucrt64\\bin\\libmpv-2.dll");
for (const std::wstring &candidate : candidates) {
if (candidate.find(L'\\') != std::wstring::npos || candidate.find(L'/') != std::wstring::npos) {
library = LoadLibraryExW(candidate.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
} else {
library = LoadLibraryW(candidate.c_str());
}
if (library) break;
}
if (!library) {
loadFailure = "Unable to load libmpv-2.dll. Bundle it under native/windows or set NUVIO_LIBMPV_PATH.";
return;
}
create = loadSymbol<mpv_create_fn>("mpv_create");
initialize = loadSymbol<mpv_initialize_fn>("mpv_initialize");
terminateDestroy = loadSymbol<mpv_terminate_destroy_fn>("mpv_terminate_destroy");
setOption = loadSymbol<mpv_set_option_fn>("mpv_set_option");
setOptionString = loadSymbol<mpv_set_option_string_fn>("mpv_set_option_string");
setProperty = loadSymbol<mpv_set_property_fn>("mpv_set_property");
setPropertyString = loadSymbol<mpv_set_property_string_fn>("mpv_set_property_string");
getProperty = loadSymbol<mpv_get_property_fn>("mpv_get_property");
command = loadSymbol<mpv_command_fn>("mpv_command");
errorString = loadSymbol<mpv_error_string_fn>("mpv_error_string");
freeValue = loadSymbol<mpv_free_fn>("mpv_free");
waitEvent = loadSymbol<mpv_wait_event_fn>("mpv_wait_event");
wakeup = loadSymbol<mpv_wakeup_fn>("mpv_wakeup");
}
template <typename T>
T loadSymbol(const char *name) {
FARPROC symbol = GetProcAddress(library, name);
if (!symbol) {
loadFailure = std::string("libmpv-2.dll is missing export ") + name + ".";
FreeLibrary(library);
library = nullptr;
throw std::runtime_error(loadFailure);
}
return reinterpret_cast<T>(symbol);
}
};
MpvApi &mpvApi() {
static MpvApi api;
api.ensureLoaded();
return api;
}
class WindowsMpvWebPlayer;
LRESULT CALLBACK messageWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK containerWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void registerWindowClasses() {
static std::once_flag once;
std::call_once(once, []() {
WNDCLASSEXW messageClass = {};
messageClass.cbSize = sizeof(messageClass);
messageClass.lpfnWndProc = messageWindowProc;
messageClass.hInstance = gModule;
messageClass.lpszClassName = kMessageWindowClass;
RegisterClassExW(&messageClass);
WNDCLASSEXW containerClass = {};
containerClass.cbSize = sizeof(containerClass);
containerClass.lpfnWndProc = containerWindowProc;
containerClass.hInstance = gModule;
containerClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
containerClass.lpszClassName = kContainerWindowClass;
RegisterClassExW(&containerClass);
});
}
std::mutex gWebView2WarmupMutex;
std::condition_variable gWebView2WarmupCv;
std::thread gWebView2WarmupThread;
DWORD gWebView2WarmupThreadId = 0;
bool gWebView2WarmupStarted = false;
bool gWebView2WarmupReady = false;
bool gWebView2WarmupSucceeded = false;
void notifyWebView2WarmupReady(bool succeeded) {
{
std::lock_guard<std::mutex> lock(gWebView2WarmupMutex);
if (!gWebView2WarmupReady) {
gWebView2WarmupReady = true;
gWebView2WarmupSucceeded = succeeded;
}
}
gWebView2WarmupCv.notify_all();
}
void runWebView2WarmupThread(std::string controlsUrl) {
{
std::lock_guard<std::mutex> lock(gWebView2WarmupMutex);
gWebView2WarmupThreadId = GetCurrentThreadId();
}
gWebView2WarmupCv.notify_all();
MSG queueProbe = {};
PeekMessageW(&queueProbe, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
bool didOleInitialize = false;
ComPtr<ICoreWebView2Environment> environment;
ComPtr<ICoreWebView2Controller> controller;
ComPtr<ICoreWebView2> webView;
EventRegistrationToken messageToken = {};
EventRegistrationToken navigationToken = {};
HRESULT oleResult = OleInitialize(nullptr);
didOleInitialize = SUCCEEDED(oleResult);
if (FAILED(oleResult)) {
notifyWebView2WarmupReady(false);
return;
}
std::wstring userDataDir = tempUserDataDirectory();
HRESULT envCallResult = CreateCoreWebView2EnvironmentWithOptions(
nullptr,
userDataDir.c_str(),
nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[&](HRESULT envResult, ICoreWebView2Environment *createdEnvironment) -> HRESULT {
if (FAILED(envResult) || !createdEnvironment) {
notifyWebView2WarmupReady(false);
PostQuitMessage(0);
return S_OK;
}
environment = createdEnvironment;
HRESULT controllerCallResult = createdEnvironment->CreateCoreWebView2Controller(
HWND_MESSAGE,
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[&](HRESULT controllerResult, ICoreWebView2Controller *createdController) -> HRESULT {
if (FAILED(controllerResult) || !createdController) {
notifyWebView2WarmupReady(false);
PostQuitMessage(0);
return S_OK;
}
controller = createdController;
controller->put_IsVisible(FALSE);
createdController->get_CoreWebView2(&webView);
if (!webView) {
notifyWebView2WarmupReady(false);
PostQuitMessage(0);
return S_OK;
}
webView->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[&](ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *args) -> HRESULT {
if (!args) return S_OK;
PWSTR messageJson = nullptr;
if (SUCCEEDED(args->get_WebMessageAsJson(&messageJson)) && messageJson) {
std::wstring message(messageJson);
CoTaskMemFree(messageJson);
if (message.find(L"controlsReady") != std::wstring::npos) {
notifyWebView2WarmupReady(true);
}
}
return S_OK;
}
).Get(),
&messageToken
);
webView->add_NavigationCompleted(
Callback<ICoreWebView2NavigationCompletedEventHandler>(
[&](ICoreWebView2 *, ICoreWebView2NavigationCompletedEventArgs *args) -> HRESULT {
BOOL navigationSucceeded = FALSE;
if (args) {
args->get_IsSuccess(&navigationSucceeded);
}
notifyWebView2WarmupReady(navigationSucceeded == TRUE);
return S_OK;
}
).Get(),
&navigationToken
);
std::wstring url = toWide(controlsUrl);
HRESULT navigateResult = webView->Navigate(url.c_str());
if (FAILED(navigateResult)) {
notifyWebView2WarmupReady(false);
PostQuitMessage(0);
}
return S_OK;
}
).Get()
);
if (FAILED(controllerCallResult)) {
notifyWebView2WarmupReady(false);
PostQuitMessage(0);
}
return S_OK;
}
).Get()
);
if (FAILED(envCallResult)) {
notifyWebView2WarmupReady(false);
} else {
MSG msg = {};
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
if (webView && messageToken.value != 0) {
webView->remove_WebMessageReceived(messageToken);
}
if (webView && navigationToken.value != 0) {
webView->remove_NavigationCompleted(navigationToken);
}
if (controller) {
controller->Close();
controller.Reset();
}
webView.Reset();
environment.Reset();
if (didOleInitialize) {
OleUninitialize();
}
}
bool startWebView2Warmup(const std::string &controlsUrl) {
{
std::lock_guard<std::mutex> lock(gWebView2WarmupMutex);
if (!gWebView2WarmupStarted) {
gWebView2WarmupStarted = true;
gWebView2WarmupReady = false;
gWebView2WarmupSucceeded = false;
gWebView2WarmupThread = std::thread(runWebView2WarmupThread, controlsUrl);
}
}
std::unique_lock<std::mutex> waitLock(gWebView2WarmupMutex);
bool completed = gWebView2WarmupCv.wait_for(
waitLock,
std::chrono::seconds(5),
[]() { return gWebView2WarmupReady; }
);
if (!completed) return false;
return gWebView2WarmupSucceeded;
}
void stopWebView2Warmup() {
std::thread threadToJoin;
DWORD threadId = 0;
{
std::unique_lock<std::mutex> lock(gWebView2WarmupMutex);
if (!gWebView2WarmupStarted) return;
gWebView2WarmupCv.wait_for(
lock,
std::chrono::seconds(1),
[]() { return gWebView2WarmupThreadId != 0; }
);
threadId = gWebView2WarmupThreadId;
}
if (threadId != 0) {
PostThreadMessageW(threadId, WM_QUIT, 0, 0);
}
{
std::lock_guard<std::mutex> lock(gWebView2WarmupMutex);
if (gWebView2WarmupThread.joinable()) {
threadToJoin = std::move(gWebView2WarmupThread);
}
}
if (threadToJoin.joinable()) {
threadToJoin.join();
}
{
std::lock_guard<std::mutex> lock(gWebView2WarmupMutex);
gWebView2WarmupStarted = false;
gWebView2WarmupReady = false;
gWebView2WarmupSucceeded = false;
gWebView2WarmupThreadId = 0;
}
}
class WindowsMpvWebPlayer : public std::enable_shared_from_this<WindowsMpvWebPlayer> {
struct InitializationState {
std::mutex mutex;
std::condition_variable cv;
bool complete = false;
std::string failure;
};
public:
void initialize(
HWND host,
const std::string &sourceUrl,
const std::vector<std::string> &headerLines,
bool playWhenReady,
long long initialPositionMs,
const std::string &controlsUrl,
JavaVM *vm,
jobject sink,
jmethodID method
) {
if (!host || !IsWindow(host)) {
throw std::runtime_error("Unable to resolve the AWT host HWND for native playback.");
}
javaVm = vm;
eventSink = sink;
eventMethod = method;
hostHwnd = host;
auto initState = std::make_shared<InitializationState>();
auto self = shared_from_this();
uiThread = std::thread(
[self, sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, initState]() {
self->runNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl, initState);
}
);
std::unique_lock<std::mutex> lock(initState->mutex);
initState->cv.wait(lock, [&]() { return initState->complete; });
if (!initState->failure.empty()) {
lock.unlock();
if (uiThread.joinable()) {
uiThread.join();
}
throw std::runtime_error(initState->failure);
}
}
void shutdown() {
if (shuttingDown.exchange(true)) {
return;
}
sendUiTask([self = shared_from_this()]() {
self->cleanupUiResources();
PostQuitMessage(0);
});
stopping.store(true);
{
std::lock_guard<std::mutex> lock(mpvMutex);
if (mpv && mpvApi().wakeup) {
mpvApi().wakeup(mpv);
}
}
if (eventThread.joinable()) {
eventThread.join();
}
{
std::lock_guard<std::mutex> lock(mpvMutex);
if (mpv) {
mpvApi().terminateDestroy(mpv);
mpv = nullptr;
}
}
if (uiThread.joinable() && GetCurrentThreadId() != uiThreadId) {
uiThread.join();
}
if (eventSink) {
bool didAttach = false;
JNIEnv *env = jniEnvDidAttach(&didAttach);
if (env) {
env->DeleteGlobalRef(eventSink);
}
if (didAttach) {
javaVm->DetachCurrentThread();
}
eventSink = nullptr;
}
eventMethod = nullptr;
javaVm = nullptr;
}
void processUiTasks() {
std::deque<std::function<void()>> tasks;
{
std::lock_guard<std::mutex> lock(uiTaskMutex);
tasks.swap(uiTasks);
}
for (auto &task : tasks) {
task();
}
}
void onTimer() {
if (shuttingDown.load()) return;
layoutNativeSubviews();
syncControls();
}
void updateControlsJson(const std::string &controlsJson) {
if (controlsJson.empty()) return;
{
std::lock_guard<std::mutex> lock(controlsMutex);
pendingControlsJson = controlsJson;
}
postUiTask([self = shared_from_this()]() {
self->flushPendingControlsJsonIfReady();
});
}
void setPaused(bool paused) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
int flag = paused ? 1 : 0;
mpvApi().setProperty(mpv, "pause", MPV_FORMAT_FLAG, &flag);
}
bool isPaused() {
return flagProperty("pause", true);
}
void seekToMilliseconds(long long positionMs) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
std::string seconds = std::to_string((double)positionMs / 1000.0);
const char *command[] = {"seek", seconds.c_str(), "absolute+keyframes", nullptr};
mpvApi().command(mpv, command);
}
void seekByMilliseconds(long long offsetMs) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
std::string seconds = std::to_string((double)offsetMs / 1000.0);
const char *command[] = {"seek", seconds.c_str(), "relative+keyframes", nullptr};
mpvApi().command(mpv, command);
}
void setSpeed(double speed) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
double clamped = std::max(0.25, std::min(4.0, speed));
mpvApi().setProperty(mpv, "speed", MPV_FORMAT_DOUBLE, &clamped);
}
double speed() {
return doubleProperty("speed", 1.0);
}
void setResizeMode(int mode) {
switch (mode) {
case 1:
case 2:
setStringProperty("panscan", "1.0");
setStringProperty("video-unscaled", "no");
break;
default:
setStringProperty("panscan", "0.0");
setStringProperty("video-unscaled", "no");
break;
}
}
long long durationMs() {
return (long long)std::llround(doubleProperty("duration", 0.0) * 1000.0);
}
long long positionMs() {
return (long long)std::llround(doubleProperty("time-pos", 0.0) * 1000.0);
}
long long bufferedPositionMs() {
double buffered = rawPositionSeconds() + cacheAheadSeconds();
return (long long)std::llround(std::max(buffered, 0.0) * 1000.0);
}
bool isLoading() {
bool paused = isPaused();
bool eofReached = isEnded();
bool idle = flagProperty("core-idle", true);
bool seeking = flagProperty("seeking", false);
bool bufferingCache = flagProperty("paused-for-cache", false);
bool fileReady = doubleProperty("duration", 0.0) > 0.0 || int64Property("track-list/count", 0) > 0;
return !fileReady || (idle && !paused && !eofReached) || seeking || bufferingCache;
}
bool isEnded() {
return flagProperty("eof-reached", false);
}
std::string audioTracksJson() {
return tracksJsonForType("audio");
}
std::string subtitleTracksJson() {
return tracksJsonForType("sub");
}
void selectAudioTrackId(int trackId) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
int64_t id = trackId;
mpvApi().setProperty(mpv, "aid", MPV_FORMAT_INT64, &id);
}
void selectSubtitleTrackId(int trackId) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
if (trackId < 0) {
mpvApi().setPropertyString(mpv, "sid", "no");
return;
}
int64_t id = trackId;
mpvApi().setProperty(mpv, "sid", MPV_FORMAT_INT64, &id);
}
void addSubtitleUrl(const std::string &url) {
if (url.empty()) return;
command({"sub-add", url, "select"});
}
void removeExternalSubtitles() {
removeExternalSubtitleTracks();
setStringProperty("sid", "no");
}
void removeExternalSubtitlesAndSelect(int trackId) {
removeExternalSubtitleTracks();
if (trackId >= 0) {
selectSubtitleTrackId(trackId);
} else {
setStringProperty("sid", "no");
}
}
void setSubtitleDelayMs(int delayMs) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
int clamped = std::max(-60000, std::min(60000, delayMs));
double delaySeconds = (double)clamped / 1000.0;
mpvApi().setProperty(mpv, "sub-delay", MPV_FORMAT_DOUBLE, &delaySeconds);
}
void applySubtitleStyle(
const std::string &textColor,
const std::string &backgroundColor,
const std::string &outlineColor,
double outlineSize,
bool bold,
double fontSize,
int subPos
) {
setStringProperty("sub-ass-override", "force");
setStringProperty("sub-color", textColor.empty() ? "#FFFFFFFF" : textColor);
setStringProperty("sub-back-color", backgroundColor.empty() ? "#00000000" : backgroundColor);
setStringProperty("sub-outline-color", outlineColor.empty() ? "#FF000000" : outlineColor);
setStringProperty(
"sub-border-style",
backgroundColor.rfind("#00", 0) == 0 ? "outline-and-shadow" : "opaque-box"
);
setStringProperty("sub-bold", bold ? "yes" : "no");
{
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
double outline = std::max(0.0, std::min(8.0, outlineSize));
double size = std::max(24.0, std::min(96.0, fontSize));
int64_t position = std::max(0, std::min(150, subPos));
mpvApi().setProperty(mpv, "sub-outline-size", MPV_FORMAT_DOUBLE, &outline);
mpvApi().setProperty(mpv, "sub-font-size", MPV_FORMAT_DOUBLE, &size);
mpvApi().setProperty(mpv, "sub-pos", MPV_FORMAT_INT64, &position);
}
}
private:
HWND hostHwnd = nullptr;
HWND containerHwnd = nullptr;
HWND messageHwnd = nullptr;
DWORD uiThreadId = 0;
bool didOleInitialize = false;
std::thread uiThread;
ComPtr<ICoreWebView2Environment> environment;
ComPtr<ICoreWebView2Controller> controller;
ComPtr<ICoreWebView2> webView;
EventRegistrationToken messageToken = {};
std::mutex uiTaskMutex;
std::deque<std::function<void()>> uiTasks;
std::mutex mpvMutex;
mpv_handle *mpv = nullptr;
std::thread eventThread;
std::atomic_bool stopping = false;
std::atomic_bool shuttingDown = false;
JavaVM *javaVm = nullptr;
jobject eventSink = nullptr;
jmethodID eventMethod = nullptr;
std::atomic_bool controlsWebReady = false;
std::mutex controlsMutex;
std::string pendingControlsJson;
double initialStartSeconds = 0.0;
friend LRESULT CALLBACK messageWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
friend LRESULT CALLBACK containerWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void runNativeUiThread(
std::string sourceUrl,
std::vector<std::string> headerLines,
bool playWhenReady,
long long initialPositionMs,
std::string controlsUrl,
std::shared_ptr<InitializationState> initState
) {
std::string failure;
try {
initializeOnNativeUiThread(sourceUrl, headerLines, playWhenReady, initialPositionMs, controlsUrl);
} catch (const std::exception &error) {
failure = error.what();
cleanupUiResources();
}
{
std::lock_guard<std::mutex> lock(initState->mutex);
initState->failure = failure;
initState->complete = true;
}
initState->cv.notify_one();
if (!failure.empty()) {
return;
}
MSG msg = {};
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
void initializeOnNativeUiThread(
const std::string &sourceUrl,
const std::vector<std::string> &headerLines,
bool playWhenReady,
long long initialPositionMs,
const std::string &controlsUrl
) {
registerWindowClasses();
uiThreadId = GetCurrentThreadId();
HRESULT oleResult = OleInitialize(nullptr);
didOleInitialize = SUCCEEDED(oleResult);
if (FAILED(oleResult)) {
throw std::runtime_error(hresultMessage("OleInitialize", oleResult));
}
messageHwnd = CreateWindowExW(
0,
kMessageWindowClass,
L"",
0,
0,
0,
0,
0,
HWND_MESSAGE,
nullptr,
gModule,
this
);
if (!messageHwnd) {
throw std::runtime_error("Unable to create Windows player message window.");
}
RECT bounds = {};
GetClientRect(hostHwnd, &bounds);
LONG width = std::max<LONG>(1, bounds.right - bounds.left);
LONG height = std::max<LONG>(1, bounds.bottom - bounds.top);
containerHwnd = CreateWindowExW(
0,
kContainerWindowClass,
L"",
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
0,
0,
width,
height,
hostHwnd,
nullptr,
gModule,
this
);
if (!containerHwnd) {
throw std::runtime_error("Unable to create native player container window.");
}
startWebView(controlsUrl);
startMpv(sourceUrl, headerLines, playWhenReady, initialPositionMs);
layoutNativeSubviews();
if (!SetTimer(messageHwnd, NUVIO_TIMER_ID, 500, nullptr)) {
throw std::runtime_error("Unable to start native player timer.");
}
}
void cleanupUiResources() {
if (messageHwnd) {
KillTimer(messageHwnd, NUVIO_TIMER_ID);
}
if (webView && messageToken.value != 0) {
webView->remove_WebMessageReceived(messageToken);
messageToken.value = 0;
}
if (controller) {
controller->Close();
controller.Reset();
}
webView.Reset();
environment.Reset();
if (containerHwnd) {
DestroyWindow(containerHwnd);
containerHwnd = nullptr;
}
if (messageHwnd) {
HWND hwnd = messageHwnd;
messageHwnd = nullptr;
DestroyWindow(hwnd);
}
if (didOleInitialize) {
OleUninitialize();
didOleInitialize = false;
}
}
void postUiTask(std::function<void()> task) {
if (shuttingDown.load()) return;
{
std::lock_guard<std::mutex> lock(uiTaskMutex);
uiTasks.push_back(std::move(task));
}
HWND target = messageHwnd;
if (target) {
PostMessageW(target, WM_NUVIO_TASK, 0, 0);
}
}
void sendUiTask(std::function<void()> task) {
if (GetCurrentThreadId() == uiThreadId || !messageHwnd) {
task();
return;
}
auto done = std::make_shared<bool>(false);
auto doneMutex = std::make_shared<std::mutex>();
auto doneCv = std::make_shared<std::condition_variable>();
{
std::lock_guard<std::mutex> lock(uiTaskMutex);
uiTasks.push_back([task = std::move(task), done, doneMutex, doneCv]() mutable {
task();
{
std::lock_guard<std::mutex> doneLock(*doneMutex);
*done = true;
}
doneCv->notify_one();
});
}
SendMessageW(messageHwnd, WM_NUVIO_TASK, 0, 0);
std::unique_lock<std::mutex> waitLock(*doneMutex);
doneCv->wait(waitLock, [&]() { return *done; });
}
void startWebView(const std::string &controlsUrl) {
std::wstring userDataDir = tempUserDataDirectory();
auto weakSelf = weak_from_this();
HRESULT result = CreateCoreWebView2EnvironmentWithOptions(
nullptr,
userDataDir.c_str(),
nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[weakSelf, controlsUrl](HRESULT envResult, ICoreWebView2Environment *createdEnvironment) -> HRESULT {
auto self = weakSelf.lock();
if (!self || self->shuttingDown.load()) return S_OK;
if (FAILED(envResult) || !createdEnvironment) {
return S_OK;
}
self->environment = createdEnvironment;
auto controllerWeakSelf = weakSelf;
HRESULT controllerResult = createdEnvironment->CreateCoreWebView2Controller(
self->containerHwnd,
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[controllerWeakSelf, controlsUrl](HRESULT controllerResult, ICoreWebView2Controller *createdController) -> HRESULT {
auto controllerSelf = controllerWeakSelf.lock();
if (!controllerSelf || controllerSelf->shuttingDown.load()) return S_OK;
if (FAILED(controllerResult) || !createdController) {
return S_OK;
}
controllerSelf->controller = createdController;
createdController->get_CoreWebView2(&controllerSelf->webView);
ComPtr<ICoreWebView2Controller2> controller2;
if (SUCCEEDED(createdController->QueryInterface(IID_PPV_ARGS(&controller2))) && controller2) {
COREWEBVIEW2_COLOR transparent = {0, 0, 0, 0};
controller2->put_DefaultBackgroundColor(transparent);
}
ComPtr<ICoreWebView2Settings> settings;
if (controllerSelf->webView && SUCCEEDED(controllerSelf->webView->get_Settings(&settings)) && settings) {
settings->put_AreDefaultContextMenusEnabled(FALSE);
settings->put_IsStatusBarEnabled(FALSE);
}
if (controllerSelf->webView) {
auto messageWeakSelf = controllerWeakSelf;
controllerSelf->webView->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[messageWeakSelf](ICoreWebView2 *, ICoreWebView2WebMessageReceivedEventArgs *args) -> HRESULT {
auto messageSelf = messageWeakSelf.lock();
if (!messageSelf || messageSelf->shuttingDown.load() || !args) return S_OK;
PWSTR messageJson = nullptr;
if (SUCCEEDED(args->get_WebMessageAsJson(&messageJson)) && messageJson) {
messageSelf->handleWebMessage(std::wstring(messageJson));
CoTaskMemFree(messageJson);
}
return S_OK;
}
).Get(),
&controllerSelf->messageToken
);
controllerSelf->layoutNativeSubviews();
std::wstring url = toWide(controlsUrl);
controllerSelf->webView->Navigate(url.c_str());
createdController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
}
return S_OK;
}
).Get()
);
(void)controllerResult;
return S_OK;
}
).Get()
);
if (FAILED(result)) {
throw std::runtime_error(hresultMessage("CreateCoreWebView2EnvironmentWithOptions", result));
}
}
void startMpv(
const std::string &sourceUrl,
const std::vector<std::string> &headerLines,
bool playWhenReady,
long long initialPositionMs
) {
MpvApi &api = mpvApi();
{
std::lock_guard<std::mutex> lock(mpvMutex);
mpv = api.create();
if (!mpv) {
throw std::runtime_error("mpv_create failed.");
}
initialStartSeconds = initialPositionMs > 0 ? (double)initialPositionMs / 1000.0 : 0.0;
setMpvOptionStringLocked("config", "no");
setMpvOptionStringLocked("osc", "no");
setMpvOptionStringLocked("input-default-bindings", "yes");
setMpvOptionStringLocked("input-vo-keyboard", "no");
setMpvOptionStringLocked("keep-open", "yes");
setMpvOptionStringLocked("vo", "gpu-next");
setMpvOptionStringLocked("gpu-api", "d3d11");
setMpvOptionStringLocked("hwdec", "d3d11va");
setMpvOptionStringLocked("hwdec-codecs", "all");
setMpvOptionStringLocked("vd-lavc-software-fallback", "no");
setMpvOptionStringLocked("vd-lavc-threads", "4");
setMpvOptionStringLocked("target-colorspace-hint", "yes");
setMpvOptionStringLocked("tone-mapping", "auto");
setMpvOptionStringLocked("dither-depth", "auto");
setMpvOptionStringLocked("deband", "yes");
setMpvOptionStringLocked("scale", "spline36");
setMpvOptionStringLocked("cscale", "spline36");
setMpvOptionStringLocked("demuxer-max-bytes", "64MiB");
setMpvOptionStringLocked("demuxer-max-back-bytes", "16MiB");
setMpvOptionStringLocked("demuxer-seekable-cache", "no");
setMpvOptionStringLocked("cache-secs", "30");
setMpvOptionStringLocked("hr-seek", "no");
int64_t wid = (int64_t)(intptr_t)containerHwnd;
int widResult = api.setOption(mpv, "wid", MPV_FORMAT_INT64, &wid);
if (widResult < 0) {
throw std::runtime_error(std::string("mpv wid option failed: ") + api.errorText(widResult));
}
if (!headerLines.empty()) {
std::string headers;
for (size_t index = 0; index < headerLines.size(); index++) {
if (index > 0) headers.push_back(',');
headers += headerLines[index];
}
setMpvOptionStringLocked("http-header-fields", headers.c_str());
}
int initResult = api.initialize(mpv);
if (initResult < 0) {
throw std::runtime_error(std::string("mpv_initialize failed: ") + api.errorText(initResult));
}
std::vector<const char *> loadCommand = {"loadfile", sourceUrl.c_str()};
std::string loadOptions;
if (initialPositionMs > 0) {
char startBuffer[64];
std::snprintf(startBuffer, sizeof(startBuffer), "start=%.3f", (double)initialPositionMs / 1000.0);
loadOptions = startBuffer;
loadCommand.push_back("replace");
loadCommand.push_back("-1");
loadCommand.push_back(loadOptions.c_str());
}
loadCommand.push_back(nullptr);
int commandResult = api.command(mpv, loadCommand.data());
if (commandResult < 0) {
throw std::runtime_error(std::string("mpv loadfile failed: ") + api.errorText(commandResult));
}
}
setPaused(!playWhenReady);
auto self = shared_from_this();
eventThread = std::thread([self]() { self->drainMpvEvents(); });
}
void layoutNativeSubviews() {
if (!hostHwnd || !IsWindow(hostHwnd)) {
return;
}
RECT bounds = {};
GetClientRect(hostHwnd, &bounds);
LONG width = std::max<LONG>(1, bounds.right - bounds.left);
LONG height = std::max<LONG>(1, bounds.bottom - bounds.top);
if (containerHwnd) {
SetWindowPos(containerHwnd, HWND_TOP, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE);
}
if (controller) {
RECT webBounds = {0, 0, width, height};
controller->put_Bounds(webBounds);
controller->put_IsVisible(TRUE);
}
}
void flushPendingControlsJsonIfReady() {
if (!webView || !controlsWebReady.load()) {
return;
}
std::string controlsJson;
{
std::lock_guard<std::mutex> lock(controlsMutex);
controlsJson = pendingControlsJson;
pendingControlsJson.clear();
}
if (controlsJson.empty()) return;
std::wstring script =
L"(function(){if(!window.playerControls)return 'missing';window.playerControls(JSON.parse(" +
javaScriptStringLiteral(controlsJson) +
L"));return 'applied';})()";
webView->ExecuteScript(script.c_str(), nullptr);
}
void syncControls() {
if (!webView) return;
double duration = doubleProperty("duration", 0.0);
double position = doubleProperty("time-pos", 0.0);
bool paused = isPaused();
bool loading = isLoading();
std::string audioTracks = audioTracksJson();
std::string subtitleTracks = subtitleTracksJson();
std::ostringstream script;
script << "window.playerUpdate({duration:" << duration
<< ",position:" << position
<< ",paused:" << (paused ? "true" : "false")
<< ",loading:" << (loading ? "true" : "false")
<< ",audioTracks:" << audioTracks
<< ",subtitleTracks:" << subtitleTracks
<< "})";
std::wstring wideScript = toWide(script.str());
webView->ExecuteScript(wideScript.c_str(), nullptr);
}
void handleWebMessage(const std::wstring &messageJson) {
std::string type = extractJsonString(messageJson, L"type");
if (type.empty()) return;
double value = extractJsonNumber(messageJson, L"value", 0.0);
if (type == "controlsReady") {
controlsWebReady.store(true);
flushPendingControlsJsonIfReady();
syncControls();
return;
}
if (type == "selectAudioTrack") {
selectAudioTrackId((int)std::llround(value));
syncControls();
return;
}
if (type == "selectSubtitleTrack") {
selectSubtitleTrackId((int)std::llround(value));
syncControls();
return;
}
sendPlayerEvent(type, value);
}
static std::string extractJsonString(const std::wstring &json, const std::wstring &field) {
std::wstring key = L"\"" + field + L"\"";
size_t keyIndex = json.find(key);
if (keyIndex == std::wstring::npos) return std::string();
size_t colon = json.find(L':', keyIndex + key.size());
if (colon == std::wstring::npos) return std::string();
size_t quote = json.find(L'"', colon + 1);
if (quote == std::wstring::npos) return std::string();
std::wstring result;
bool escaping = false;
for (size_t index = quote + 1; index < json.size(); index++) {
wchar_t ch = json[index];
if (escaping) {
switch (ch) {
case L'"': result.push_back(L'"'); break;
case L'\\': result.push_back(L'\\'); break;
case L'/': result.push_back(L'/'); break;
case L'b': result.push_back(L'\b'); break;
case L'f': result.push_back(L'\f'); break;
case L'n': result.push_back(L'\n'); break;
case L'r': result.push_back(L'\r'); break;
case L't': result.push_back(L'\t'); break;
default: result.push_back(ch); break;
}
escaping = false;
continue;
}
if (ch == L'\\') {
escaping = true;
continue;
}
if (ch == L'"') break;
result.push_back(ch);
}
return toUtf8(result);
}
static double extractJsonNumber(const std::wstring &json, const std::wstring &field, double fallback) {
std::wstring key = L"\"" + field + L"\"";
size_t keyIndex = json.find(key);
if (keyIndex == std::wstring::npos) return fallback;
size_t colon = json.find(L':', keyIndex + key.size());
if (colon == std::wstring::npos) return fallback;
size_t start = json.find_first_not_of(L" \t\r\n", colon + 1);
if (start == std::wstring::npos) return fallback;
size_t end = start;
while (end < json.size()) {
wchar_t ch = json[end];
if (!(std::iswdigit(ch) || ch == L'-' || ch == L'+' || ch == L'.' || ch == L'e' || ch == L'E')) {
break;
}
end++;
}
if (end <= start) return fallback;
try {
return std::stod(json.substr(start, end - start));
} catch (...) {
return fallback;
}
}
void drainMpvEvents() {
while (!stopping.load()) {
mpv_handle *current = nullptr;
{
std::lock_guard<std::mutex> lock(mpvMutex);
current = mpv;
}
if (!current) {
return;
}
mpv_event *event = mpvApi().waitEvent(current, 0.5);
if (!event) continue;
if (event->event_id == MPV_EVENT_SHUTDOWN) {
return;
}
}
}
JNIEnv *jniEnvDidAttach(bool *didAttach) {
if (didAttach) *didAttach = false;
if (!javaVm) return nullptr;
JNIEnv *env = nullptr;
jint status = javaVm->GetEnv((void **)&env, JNI_VERSION_1_6);
if (status == JNI_OK) return env;
if (status != JNI_EDETACHED) return nullptr;
if (javaVm->AttachCurrentThread((void **)&env, nullptr) != JNI_OK) {
return nullptr;
}
if (didAttach) *didAttach = true;
return env;
}
void sendPlayerEvent(const std::string &type, double value) {
if (!eventSink || !eventMethod) return;
bool didAttach = false;
JNIEnv *env = jniEnvDidAttach(&didAttach);
if (!env) return;
jstring eventType = newJavaStringUtf8(env, type);
env->CallVoidMethod(eventSink, eventMethod, eventType, (jdouble)value);
if (env->ExceptionCheck()) {
env->ExceptionClear();
}
if (eventType) {
env->DeleteLocalRef(eventType);
}
if (didAttach) {
javaVm->DetachCurrentThread();
}
}
void setMpvOptionStringLocked(const char *name, const char *value) {
(void)mpvApi().setOptionString(mpv, name, value);
}
double doubleProperty(const char *name, double fallback) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return fallback;
double value = fallback;
int result = mpvApi().getProperty(mpv, name, MPV_FORMAT_DOUBLE, &value);
if (result < 0) {
return fallback;
}
return value;
}
long long int64Property(const char *name, long long fallback) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return fallback;
int64_t value = fallback;
int result = mpvApi().getProperty(mpv, name, MPV_FORMAT_INT64, &value);
if (result < 0) {
return fallback;
}
return value;
}
bool flagProperty(const char *name, bool fallback) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return fallback;
int flag = fallback ? 1 : 0;
int result = mpvApi().getProperty(mpv, name, MPV_FORMAT_FLAG, &flag);
if (result < 0) {
return fallback;
}
return flag != 0;
}
std::string stringProperty(const char *name, const std::string &fallback) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return fallback;
char *value = nullptr;
int propertyResult = mpvApi().getProperty(mpv, name, MPV_FORMAT_STRING, &value);
if (propertyResult < 0 || !value) {
return fallback;
}
std::string result(value);
mpvApi().freeValue(value);
return result;
}
void setStringProperty(const char *name, const std::string &value) {
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
mpvApi().setPropertyString(mpv, name, value.c_str());
}
void command(const std::vector<std::string> &args) {
if (args.empty()) return;
std::lock_guard<std::mutex> lock(mpvMutex);
if (!mpv) return;
std::vector<const char *> cargs;
cargs.reserve(args.size() + 1);
for (const std::string &arg : args) {
cargs.push_back(arg.c_str());
}
cargs.push_back(nullptr);
mpvApi().command(mpv, cargs.data());
}
double rawPositionSeconds() {
double position = doubleProperty("time-pos", 0.0);
return std::isfinite(position) ? std::max(position, 0.0) : 0.0;
}
double effectiveCachePositionSeconds() {
double position = rawPositionSeconds();
if (initialStartSeconds > 0.0 && position + 5.0 < initialStartSeconds) {
return initialStartSeconds;
}
return position;
}
double cacheAheadSeconds() {
double effectivePosition = effectiveCachePositionSeconds();
double cacheTime = doubleProperty("demuxer-cache-time", 0.0);
if (std::isfinite(cacheTime) && cacheTime > 0.0) {
if (cacheTime >= effectivePosition - 5.0) {
return std::max(cacheTime - effectivePosition, 0.0);
}
return cacheTime;
}
double cacheDuration = doubleProperty("demuxer-cache-duration", 0.0);
if (std::isfinite(cacheDuration) && cacheDuration > 0.0) {
return cacheDuration;
}
return 0.0;
}
void removeExternalSubtitleTracks() {
long long count = int64Property("track-list/count", 0);
if (count <= 0) return;
for (long long index = count - 1; index >= 0; index--) {
std::string prefix = "track-list/" + std::to_string(index);
std::string type = stringProperty((prefix + "/type").c_str(), "");
bool external = flagProperty((prefix + "/external").c_str(), false);
if (type == "sub" && external) {
long long trackId = int64Property((prefix + "/id").c_str(), -1);
if (trackId >= 0) {
command({"sub-remove", std::to_string(trackId)});
}
}
}
}
std::string tracksJsonForType(const std::string &wantedType) {
long long count = int64Property("track-list/count", 0);
std::ostringstream json;
json << "[";
int logicalIndex = 0;
bool first = true;
for (long long index = 0; index < count; index++) {
std::string prefix = "track-list/" + std::to_string(index);
std::string type = stringProperty((prefix + "/type").c_str(), "");
if (type != wantedType) continue;
long long trackId = int64Property((prefix + "/id").c_str(), logicalIndex + 1);
std::string title = trackStringAtIndex(index, "title");
std::string language = trackStringAtIndex(index, "lang");
std::string codec = trackStringAtIndex(index, "codec");
std::string decoderDescription = trackStringAtIndex(index, "decoder-desc");
std::string channels = trackStringAtIndex(index, "demux-channels");
long long channelCount = int64Property((prefix + "/demux-channel-count").c_str(), 0);
bool selected = flagProperty((prefix + "/selected").c_str(), false);
bool forced = flagProperty((prefix + "/forced").c_str(), false);
std::string label = formatTrackTitle(type, logicalIndex, title, language, codec, decoderDescription, channels, (int)channelCount);
if (!first) json << ",";
first = false;
json << "{"
<< "\"index\":" << logicalIndex << ","
<< "\"id\":\"" << jsonEscape(std::to_string(trackId)) << "\","
<< "\"label\":\"" << jsonEscape(label) << "\","
<< "\"language\":\"" << jsonEscape(language) << "\","
<< "\"selected\":" << (selected ? "true" : "false") << ","
<< "\"forced\":" << (forced ? "true" : "false")
<< "}";
logicalIndex++;
}
json << "]";
return json.str();
}
std::string trackStringAtIndex(long long index, const std::string &field) {
return trim(stringProperty(("track-list/" + std::to_string(index) + "/" + field).c_str(), ""));
}
std::string formatTrackTitle(
const std::string &type,
int index,
const std::string &title,
const std::string &language,
const std::string &codec,
const std::string &decoderDescription,
const std::string &channels,
int channelCount
) {
std::string base = !trim(title).empty()
? trim(title)
: (!trim(language).empty()
? trim(language)
: (type == "sub" ? "Subtitle " + std::to_string(index + 1) : "Track " + std::to_string(index + 1)));
std::string codecName = codecDisplayName(codec);
if (codecName.empty()) codecName = codecDisplayName(decoderDescription);
std::string channelName = type == "audio" ? channelLayoutName(channels, channelCount) : "";
std::vector<std::string> details;
for (const std::string &detail : {channelName, codecName}) {
if (!detail.empty() && !containsCaseInsensitive(base, detail)) {
details.push_back(detail);
}
}
if (details.empty()) return base;
std::string suffix;
for (size_t detailIndex = 0; detailIndex < details.size(); detailIndex++) {
if (detailIndex > 0) suffix += ", ";
suffix += details[detailIndex];
}
return base + " (" + suffix + ")";
}
std::string channelLayoutName(const std::string &channels, int channelCount) {
std::string normalized = trim(channels);
if (!normalized.empty() && lowerCopy(normalized) != "unknown") {
std::string lower = lowerCopy(normalized);
if (lower == "mono") return "Mono";
if (lower == "stereo") return "Stereo";
return normalized;
}
switch (channelCount) {
case 1: return "Mono";
case 2: return "Stereo";
case 6: return "5.1";
case 8: return "7.1";
default: return channelCount > 0 ? std::to_string(channelCount) + "ch" : "";
}
}
std::string codecDisplayName(const std::string &value) {
std::string raw = trim(value);
if (raw.empty()) return "";
std::string codec = lowerCopy(raw);
if (codec.find("eac3") != std::string::npos || codec.find("e-ac-3") != std::string::npos || codec.find("e ac-3") != std::string::npos) {
return codec.find("joc") != std::string::npos || codec.find("atmos") != std::string::npos ? "E-AC-3-JOC" : "E-AC-3";
}
if (codec.find("truehd") != std::string::npos || codec.find("true hd") != std::string::npos) return "TrueHD";
if (codec.find("ac3") != std::string::npos || codec.find("ac-3") != std::string::npos) return "AC-3";
if (codec.find("dts-hd") != std::string::npos || codec.find("dtshd") != std::string::npos || codec.find("dts hd") != std::string::npos) return "DTS-HD";
if (codec.find("dts") != std::string::npos || codec == "dca") return "DTS";
if (codec.find("aac") != std::string::npos) return "AAC";
if (codec.find("mp3") != std::string::npos || codec.find("mpeg audio") != std::string::npos) return "MP3";
if (codec.find("mp2") != std::string::npos) return "MP2";
if (codec.find("opus") != std::string::npos) return "Opus";
if (codec.find("vorbis") != std::string::npos) return "Vorbis";
if (codec.find("flac") != std::string::npos) return "FLAC";
if (codec.find("alac") != std::string::npos) return "ALAC";
if (codec.find("pcm") != std::string::npos || codec.find("wav") != std::string::npos) return "WAV";
if (codec.find("pgs") != std::string::npos || codec.find("hdmv") != std::string::npos) return "PGS";
if (codec.find("subrip") != std::string::npos || codec == "srt") return "SRT";
if (codec.find("ass") != std::string::npos || codec.find("ssa") != std::string::npos) return "SSA";
if (codec.find("webvtt") != std::string::npos || codec == "vtt") return "VTT";
if (codec.find("ttml") != std::string::npos) return "TTML";
if (codec.find("mov_text") != std::string::npos || codec.find("tx3g") != std::string::npos) return "TX3G";
if (codec.find("dvb") != std::string::npos) return "DVB";
return raw;
}
};
LRESULT CALLBACK messageWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
auto *create = reinterpret_cast<CREATESTRUCTW *>(lParam);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
return TRUE;
}
auto *player = reinterpret_cast<WindowsMpvWebPlayer *>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_NUVIO_TASK:
if (player) player->processUiTasks();
return 0;
case WM_TIMER:
if (player && wParam == NUVIO_TIMER_ID) player->onTimer();
return 0;
case WM_NCDESTROY:
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
return 0;
default:
return DefWindowProcW(hwnd, message, wParam, lParam);
}
}
LRESULT CALLBACK containerWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
auto *create = reinterpret_cast<CREATESTRUCTW *>(lParam);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
return TRUE;
}
switch (message) {
case WM_SIZE:
return 0;
case WM_ERASEBKGND: {
RECT rect = {};
GetClientRect(hwnd, &rect);
FillRect((HDC)wParam, &rect, (HBRUSH)GetStockObject(BLACK_BRUSH));
return 1;
}
case WM_NCDESTROY:
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
return 0;
default:
return DefWindowProcW(hwnd, message, wParam, lParam);
}
}
std::shared_ptr<WindowsMpvWebPlayer> playerFromHandle(jlong handle) {
if (handle == 0) return nullptr;
auto *holder = reinterpret_cast<std::shared_ptr<WindowsMpvWebPlayer> *>(handle);
return holder ? *holder : nullptr;
}
} // namespace
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
gModule = module;
DisableThreadLibraryCalls(module);
}
return TRUE;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_create(
JNIEnv *env,
jobject,
jlong hostViewPtr,
jstring sourceUrl,
jobjectArray headerLines,
jboolean playWhenReady,
jlong initialPositionMs,
jstring controlsPageUrl,
jobject eventSink
) {
HWND hostHwnd = (HWND)(intptr_t)hostViewPtr;
std::string sourceUrlText = jstringToUtf8(env, sourceUrl);
std::vector<std::string> headerLineValues = jstringArrayToVector(env, headerLines);
std::string controlsPageUrlText = jstringToUtf8(env, controlsPageUrl);
JavaVM *javaVm = nullptr;
env->GetJavaVM(&javaVm);
jobject eventSinkRef = nullptr;
jmethodID eventMethod = nullptr;
if (eventSink) {
eventSinkRef = env->NewGlobalRef(eventSink);
jclass eventSinkClass = env->GetObjectClass(eventSink);
eventMethod = env->GetMethodID(eventSinkClass, "onPlayerEvent", "(Ljava/lang/String;D)V");
env->DeleteLocalRef(eventSinkClass);
if (!eventMethod) {
if (eventSinkRef) env->DeleteGlobalRef(eventSinkRef);
throwJavaError(env, "Native player event sink is missing onPlayerEvent(String, Double).");
return 0;
}
}
auto player = std::make_shared<WindowsMpvWebPlayer>();
try {
player->initialize(
hostHwnd,
sourceUrlText,
headerLineValues,
playWhenReady == JNI_TRUE,
initialPositionMs,
controlsPageUrlText,
javaVm,
eventSinkRef,
eventMethod
);
} catch (const std::exception &error) {
player->shutdown();
throwJavaError(env, error.what());
return 0;
}
auto *holder = new std::shared_ptr<WindowsMpvWebPlayer>(player);
jlong handle = (jlong)(intptr_t)holder;
return handle;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_warmupWebView2(JNIEnv *env, jobject, jstring controlsPageUrl) {
std::string controlsPageUrlText = jstringToUtf8(env, controlsPageUrl);
return startWebView2Warmup(controlsPageUrlText) ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_shutdownWebView2Warmup(JNIEnv *, jobject) {
stopWebView2Warmup();
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_dispose(JNIEnv *, jobject, jlong handle) {
if (handle == 0) return;
auto *holder = reinterpret_cast<std::shared_ptr<WindowsMpvWebPlayer> *>(handle);
std::shared_ptr<WindowsMpvWebPlayer> player = *holder;
delete holder;
if (player) player->shutdown();
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_updateControls(JNIEnv *env, jobject, jlong handle, jstring controlsJson) {
auto player = playerFromHandle(handle);
std::string controlsJsonText = jstringToUtf8(env, controlsJson);
if (player) player->updateControlsJson(controlsJsonText);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_setPaused(JNIEnv *, jobject, jlong handle, jboolean paused) {
auto player = playerFromHandle(handle);
if (player) player->setPaused(paused == JNI_TRUE);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_seekTo(JNIEnv *, jobject, jlong handle, jlong positionMs) {
auto player = playerFromHandle(handle);
if (player) player->seekToMilliseconds(positionMs);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_seekBy(JNIEnv *, jobject, jlong handle, jlong offsetMs) {
auto player = playerFromHandle(handle);
if (player) player->seekByMilliseconds(offsetMs);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_setSpeed(JNIEnv *, jobject, jlong handle, jfloat speed) {
auto player = playerFromHandle(handle);
if (player) player->setSpeed(speed);
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_durationMs(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player ? player->durationMs() : 0;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_positionMs(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player ? player->positionMs() : 0;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_bufferedPositionMs(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player ? player->bufferedPositionMs() : 0;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_isLoading(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player && player->isLoading() ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_isEnded(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player && player->isEnded() ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_isPaused(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return !player || player->isPaused() ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT jfloat JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_speed(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return player ? (jfloat)player->speed() : 1.0f;
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_setResizeMode(JNIEnv *, jobject, jlong handle, jint mode) {
auto player = playerFromHandle(handle);
if (player) player->setResizeMode(mode);
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_audioTracksJson(JNIEnv *env, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return newJavaStringUtf8(env, player ? player->audioTracksJson() : "[]");
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_subtitleTracksJson(JNIEnv *env, jobject, jlong handle) {
auto player = playerFromHandle(handle);
return newJavaStringUtf8(env, player ? player->subtitleTracksJson() : "[]");
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_selectAudioTrack(JNIEnv *, jobject, jlong handle, jint trackId) {
auto player = playerFromHandle(handle);
if (player) player->selectAudioTrackId(trackId);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_selectSubtitleTrack(JNIEnv *, jobject, jlong handle, jint trackId) {
auto player = playerFromHandle(handle);
if (player) player->selectSubtitleTrackId(trackId);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_addSubtitleUrl(JNIEnv *env, jobject, jlong handle, jstring url) {
auto player = playerFromHandle(handle);
if (player) player->addSubtitleUrl(jstringToUtf8(env, url));
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_clearExternalSubtitles(JNIEnv *, jobject, jlong handle) {
auto player = playerFromHandle(handle);
if (player) player->removeExternalSubtitles();
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_clearExternalSubtitlesAndSelect(JNIEnv *, jobject, jlong handle, jint trackId) {
auto player = playerFromHandle(handle);
if (player) player->removeExternalSubtitlesAndSelect(trackId);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_applyWindowChrome(
JNIEnv *,
jobject,
jlong windowHwnd,
jboolean darkMode,
jint captionColorRgb,
jint borderColorRgb,
jint textColorRgb
) {
applyDwmWindowChrome(
(HWND)(intptr_t)windowHwnd,
darkMode == JNI_TRUE,
rgbIntToColorRef(captionColorRgb),
rgbIntToColorRef(borderColorRgb),
rgbIntToColorRef(textColorRgb)
);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_setSubtitleDelayMs(JNIEnv *, jobject, jlong handle, jint delayMs) {
auto player = playerFromHandle(handle);
if (player) player->setSubtitleDelayMs(delayMs);
}
extern "C" JNIEXPORT void JNICALL
Java_com_nuvio_app_features_player_desktop_NativePlayerBridge_applySubtitleStyle(
JNIEnv *env,
jobject,
jlong handle,
jstring textColor,
jstring backgroundColor,
jstring outlineColor,
jfloat outlineSize,
jboolean bold,
jfloat fontSize,
jint subPos
) {
auto player = playerFromHandle(handle);
if (!player) return;
player->applySubtitleStyle(
jstringToUtf8(env, textColor),
jstringToUtf8(env, backgroundColor),
jstringToUtf8(env, outlineColor),
outlineSize,
bold == JNI_TRUE,
fontSize,
subPos
);
}