From 5f99c90b903e8f2365612586146c4182e7ff57c8 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:41:49 +0200 Subject: [PATCH 1/7] Inline native sources and build them in CI - Remove winlib git submodule, move Windows C++ sources to mediaplayer/src/jvmMain/native/windows/ - Move macOS Swift source to mediaplayer/src/jvmMain/native/macos/ - Add build.sh for macOS and update build.bat/CMakeLists.txt for new paths - Add reusable build-natives.yml CI workflow (Windows + macOS in parallel) - Update build-test.yml and publish-on-maven-central.yml to use build-natives - Update mediaplayer/build.gradle.kts with Exec tasks (ComposeDeskKit pattern) - Remove compiled binaries from git, build them in CI instead --- .github/workflows/build-natives.yml | 61 + .github/workflows/build-test.yml | 42 +- .../workflows/publish-on-maven-central.yml | 44 +- .gitignore | 20 +- .gitmodules | 3 - mediaplayer/build.gradle.kts | 66 +- .../macos}/NativeVideoPlayer.swift | 0 mediaplayer/src/jvmMain/native/macos/build.sh | 37 + .../jvmMain/native/windows/AudioManager.cpp | 340 +++++ .../src/jvmMain/native/windows/AudioManager.h | 72 + .../src/jvmMain/native/windows/CMakeLists.txt | 72 + .../native/windows/MediaFoundationManager.cpp | 130 ++ .../native/windows/MediaFoundationManager.h | 74 + .../native/windows/NativeVideoPlayer.cpp | 1349 +++++++++++++++++ .../native/windows/NativeVideoPlayer.h | 239 +++ .../src/jvmMain/native/windows/Utils.cpp | 24 + .../src/jvmMain/native/windows/Utils.h | 23 + .../native/windows/VideoPlayerInstance.h | 59 + .../src/jvmMain/native/windows/build.bat | 49 + .../darwin-aarch64/libNativeVideoPlayer.dylib | Bin 201560 -> 0 bytes .../darwin-x86-64/libNativeVideoPlayer.dylib | Bin 179624 -> 0 bytes .../win32-arm64/NativeVideoPlayer.dll | Bin 29184 -> 0 bytes .../win32-x86-64/NativeVideoPlayer.dll | Bin 31744 -> 0 bytes winlib | 1 - 24 files changed, 2655 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/build-natives.yml delete mode 100644 .gitmodules rename mediaplayer/src/jvmMain/{kotlin/io/github/kdroidfilter/composemediaplayer/mac/native => native/macos}/NativeVideoPlayer.swift (100%) create mode 100644 mediaplayer/src/jvmMain/native/macos/build.sh create mode 100644 mediaplayer/src/jvmMain/native/windows/AudioManager.cpp create mode 100644 mediaplayer/src/jvmMain/native/windows/AudioManager.h create mode 100644 mediaplayer/src/jvmMain/native/windows/CMakeLists.txt create mode 100644 mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp create mode 100644 mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h create mode 100644 mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp create mode 100644 mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h create mode 100644 mediaplayer/src/jvmMain/native/windows/Utils.cpp create mode 100644 mediaplayer/src/jvmMain/native/windows/Utils.h create mode 100644 mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h create mode 100644 mediaplayer/src/jvmMain/native/windows/build.bat delete mode 100755 mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib delete mode 100755 mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib delete mode 100644 mediaplayer/src/jvmMain/resources/win32-arm64/NativeVideoPlayer.dll delete mode 100644 mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll delete mode 160000 winlib diff --git a/.github/workflows/build-natives.yml b/.github/workflows/build-natives.yml new file mode 100644 index 00000000..8bf6d6f8 --- /dev/null +++ b/.github/workflows/build-natives.yml @@ -0,0 +1,61 @@ +name: Build Native Libraries + +on: + workflow_call: + +jobs: + windows: + runs-on: windows-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Build Windows native DLLs + shell: cmd + run: call mediaplayer\src\jvmMain\native\windows\build.bat + + - name: Verify Windows natives + shell: bash + run: | + for f in \ + mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll \ + mediaplayer/src/jvmMain/resources/win32-arm64/NativeVideoPlayer.dll; do + if [ ! -f "$f" ]; then echo "MISSING: $f" >&2; exit 1; fi + echo "OK: $f ($(wc -c < "$f") bytes)" + done + + - name: Upload Windows DLLs + uses: actions/upload-artifact@v4 + with: + name: windows-natives + path: | + mediaplayer/src/jvmMain/resources/win32-x86-64/ + mediaplayer/src/jvmMain/resources/win32-arm64/ + retention-days: 1 + + macos: + runs-on: macos-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Build macOS native dylibs + run: bash mediaplayer/src/jvmMain/native/macos/build.sh + + - name: Verify macOS natives + run: | + for f in \ + mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib \ + mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib; do + if [ ! -f "$f" ]; then echo "MISSING: $f" >&2; exit 1; fi + echo "OK: $f ($(wc -c < "$f") bytes)" + done + + - name: Upload macOS dylibs + uses: actions/upload-artifact@v4 + with: + name: macos-natives + path: | + mediaplayer/src/jvmMain/resources/darwin-aarch64/ + mediaplayer/src/jvmMain/resources/darwin-x86-64/ + retention-days: 1 diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index bf849132..f3d9d605 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -5,7 +5,11 @@ on: branches: [ master ] jobs: + build-natives: + uses: ./.github/workflows/build-natives.yml + build-and-test: + needs: build-natives strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -17,6 +21,40 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Download Windows natives + uses: actions/download-artifact@v4 + with: + name: windows-natives + path: mediaplayer/src/jvmMain/resources/ + merge-multiple: true + + - name: Download macOS natives + uses: actions/download-artifact@v4 + with: + name: macos-natives + path: mediaplayer/src/jvmMain/resources/ + merge-multiple: true + + - name: Verify all natives present + shell: bash + run: | + EXPECTED=( + "mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll" + "mediaplayer/src/jvmMain/resources/win32-arm64/NativeVideoPlayer.dll" + "mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib" + "mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib" + ) + MISSING=0 + for f in "${EXPECTED[@]}"; do + if [ -f "$f" ]; then + echo "OK: $f ($(wc -c < "$f") bytes)" + else + echo "MISSING: $f" >&2 + MISSING=1 + fi + done + if [ "$MISSING" = "1" ]; then exit 1; fi + - name: Set up JDK uses: actions/setup-java@v4 with: @@ -28,10 +66,6 @@ jobs: run: chmod +x gradlew if: runner.os != 'Windows' - - name: Grant execute permission for gradlew (Windows) - run: git update-index --chmod=+x gradlew - if: runner.os == 'Windows' - - name: Build and test with Gradle run: ./gradlew build test --no-daemon shell: bash diff --git a/.github/workflows/publish-on-maven-central.yml b/.github/workflows/publish-on-maven-central.yml index a2d2ee92..04879e9f 100644 --- a/.github/workflows/publish-on-maven-central.yml +++ b/.github/workflows/publish-on-maven-central.yml @@ -6,20 +6,57 @@ on: - 'v*' jobs: + build-natives: + uses: ./.github/workflows/build-natives.yml + publish: + needs: build-natives runs-on: macos-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Download Windows natives + uses: actions/download-artifact@v4 + with: + name: windows-natives + path: mediaplayer/src/jvmMain/resources/ + merge-multiple: true + + - name: Download macOS natives + uses: actions/download-artifact@v4 + with: + name: macos-natives + path: mediaplayer/src/jvmMain/resources/ + merge-multiple: true + + - name: Verify all natives present + run: | + EXPECTED=( + "mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll" + "mediaplayer/src/jvmMain/resources/win32-arm64/NativeVideoPlayer.dll" + "mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib" + "mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib" + ) + MISSING=0 + for f in "${EXPECTED[@]}"; do + if [ -f "$f" ]; then + echo "OK: $f ($(wc -c < "$f") bytes)" + else + echo "MISSING: $f" >&2 + MISSING=1 + fi + done + if [ "$MISSING" = "1" ]; then exit 1; fi - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - - name: Set up Publish to Maven Central + - name: Publish to Maven Central run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVENCENTRALUSERNAME }} @@ -27,4 +64,3 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNINGINMEMORYKEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNINGKEYID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNINGPASSWORD }} - diff --git a/.gitignore b/.gitignore index 46e12b98..db588951 100644 --- a/.gitignore +++ b/.gitignore @@ -15,16 +15,14 @@ Pods/ *.jks *.gpg *yarn.lock -/winlib/cmake-build-arm64 -/winlib/cmake-build-x64 -/winlib/.idea -/mediaplayer/src/jvmMain/resources/darwin-x86-64/NativeVideoPlayer.abi.json -/mediaplayer/src/jvmMain/resources/darwin-x86-64/NativeVideoPlayer.swiftdoc -/mediaplayer/src/jvmMain/resources/darwin-x86-64/NativeVideoPlayer.swiftmodule -/mediaplayer/src/jvmMain/resources/darwin-x86-64/NativeVideoPlayer.swiftsourceinfo -/mediaplayer/src/jvmMain/resources/darwin-aarch64/NativeVideoPlayer.abi.json -/mediaplayer/src/jvmMain/resources/darwin-aarch64/NativeVideoPlayer.swiftdoc -/mediaplayer/src/jvmMain/resources/darwin-aarch64/NativeVideoPlayer.swiftmodule -/mediaplayer/src/jvmMain/resources/darwin-aarch64/NativeVideoPlayer.swiftsourceinfo +# Native compiled binaries (built in CI) +/mediaplayer/src/jvmMain/resources/win32-x86-64/ +/mediaplayer/src/jvmMain/resources/win32-arm64/ +/mediaplayer/src/jvmMain/resources/darwin-aarch64/ +/mediaplayer/src/jvmMain/resources/darwin-x86-64/ + +# Native build artifacts +/mediaplayer/src/jvmMain/native/windows/build-x64/ +/mediaplayer/src/jvmMain/native/windows/build-arm64/ *.log /sample/composeApp/debug/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index cb6d8716..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "winlib"] - path = winlib - url = https://github.com/kdroidFilter/Compose-Media-Player-WinLib diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index be40107b..d73e40a4 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -1,6 +1,7 @@ @file:OptIn(ExperimentalWasmDsl::class) import com.vanniktech.maven.publish.SonatypeHost +import org.apache.tools.ant.taskdefs.condition.Os import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType @@ -159,39 +160,50 @@ android { } } -val buildMacArm: TaskProvider = tasks.register("buildNativeMacArm") { - onlyIf { System.getProperty("os.name").startsWith("Mac") } - workingDir(rootDir) - commandLine( - "swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer", - "-target", "arm64-apple-macosx14.0", - "-o", "mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib", - "mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift", - "-O", "-whole-module-optimization" - ) +val nativeResourceDir = layout.projectDirectory.dir("src/jvmMain/resources") + +val buildNativeMacOs by tasks.registering(Exec::class) { + description = "Compiles the Swift native library into macOS dylibs (arm64 + x64)" + group = "build" + val hasPrebuilt = nativeResourceDir + .dir("darwin-aarch64") + .file("libNativeVideoPlayer.dylib") + .asFile + .exists() + enabled = Os.isFamily(Os.FAMILY_MAC) && !hasPrebuilt + + val nativeDir = layout.projectDirectory.dir("src/jvmMain/native/macos") + inputs.dir(nativeDir) + outputs.dir(nativeResourceDir) + workingDir(nativeDir) + commandLine("bash", "build.sh") } -val buildMacX64: TaskProvider = tasks.register("buildNativeMacX64") { - onlyIf { System.getProperty("os.name").startsWith("Mac") } - workingDir(rootDir) - commandLine( - "swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer", - "-target", "x86_64-apple-macosx14.0", - "-o", "mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib", - "mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift", - "-O", "-whole-module-optimization" - ) +val buildNativeWindows by tasks.registering(Exec::class) { + description = "Compiles the C++ native library into Windows DLLs (x64 + ARM64)" + group = "build" + val hasPrebuilt = nativeResourceDir + .dir("win32-x86-64") + .file("NativeVideoPlayer.dll") + .asFile + .exists() + enabled = Os.isFamily(Os.FAMILY_WINDOWS) && !hasPrebuilt + + val nativeDir = layout.projectDirectory.dir("src/jvmMain/native/windows") + inputs.dir(nativeDir) + outputs.dir(nativeResourceDir) + workingDir(nativeDir) + commandLine("cmd", "/c", nativeDir.file("build.bat").asFile.absolutePath) } -val buildWin: TaskProvider = tasks.register("buildNativeWin") { - onlyIf { System.getProperty("os.name").startsWith("Windows") } - workingDir(rootDir.resolve("winlib")) - commandLine("cmd", "/c", "build.bat") +tasks.processResources { + dependsOn(buildNativeMacOs, buildNativeWindows) } -// tâche d’agrégation -tasks.register("buildNativeLibraries") { - dependsOn(buildMacArm, buildMacX64, buildWin) +tasks.configureEach { + if (name == "sourcesJar") { + dependsOn(buildNativeMacOs, buildNativeWindows) + } } diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift b/mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift similarity index 100% rename from mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift rename to mediaplayer/src/jvmMain/native/macos/NativeVideoPlayer.swift diff --git a/mediaplayer/src/jvmMain/native/macos/build.sh b/mediaplayer/src/jvmMain/native/macos/build.sh new file mode 100644 index 00000000..9d5f124f --- /dev/null +++ b/mediaplayer/src/jvmMain/native/macos/build.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +RESOURCES_DIR="$SCRIPT_DIR/../../resources" + +SWIFT_SOURCE="$SCRIPT_DIR/NativeVideoPlayer.swift" + +# Output directories (JNA resource path convention) +ARM64_DIR="$RESOURCES_DIR/darwin-aarch64" +X64_DIR="$RESOURCES_DIR/darwin-x86-64" + +mkdir -p "$ARM64_DIR" "$X64_DIR" + +echo "=== Building NativeVideoPlayer for macOS arm64 ===" +swiftc -emit-library -emit-module -module-name NativeVideoPlayer \ + -target arm64-apple-macosx14.0 \ + -o "$ARM64_DIR/libNativeVideoPlayer.dylib" \ + "$SWIFT_SOURCE" \ + -O -whole-module-optimization + +echo "=== Building NativeVideoPlayer for macOS x86_64 ===" +swiftc -emit-library -emit-module -module-name NativeVideoPlayer \ + -target x86_64-apple-macosx14.0 \ + -o "$X64_DIR/libNativeVideoPlayer.dylib" \ + "$SWIFT_SOURCE" \ + -O -whole-module-optimization + +# Clean up swift build artifacts +rm -f "$ARM64_DIR"/NativeVideoPlayer.abi.json "$ARM64_DIR"/NativeVideoPlayer.swiftdoc \ + "$ARM64_DIR"/NativeVideoPlayer.swiftmodule "$ARM64_DIR"/NativeVideoPlayer.swiftsourceinfo +rm -f "$X64_DIR"/NativeVideoPlayer.abi.json "$X64_DIR"/NativeVideoPlayer.swiftdoc \ + "$X64_DIR"/NativeVideoPlayer.swiftmodule "$X64_DIR"/NativeVideoPlayer.swiftsourceinfo + +echo "=== Build completed ===" +echo "arm64: $ARM64_DIR/libNativeVideoPlayer.dylib" +echo "x86_64: $X64_DIR/libNativeVideoPlayer.dylib" diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp new file mode 100644 index 00000000..f83af94d --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.cpp @@ -0,0 +1,340 @@ +// AudioManager_improved.cpp – full rewrite with tighter A/V synchronisation +// ----------------------------------------------------------------------------- +// * Keeps the original public API so that existing call‑sites still compile. +// * Uses an event‑driven render loop instead of busy‑wait polling where possible. +// * Measures drift between the WASAPI render clock and the Media Foundation +// presentation clock and corrects it gradually to avoid audible glitches. +// * All sleeps are clamped to a minimum of 1 ms to keep the thread responsive. +// * Volume scaling is done in place only when necessary and supports both +// 16‑bit and 32‑bit (float) PCM formats. +// ----------------------------------------------------------------------------- + +#include "AudioManager.h" +#include "VideoPlayerInstance.h" +#include "Utils.h" +#include "MediaFoundationManager.h" +#include +#include +#include + +using namespace VideoPlayerUtils; + +namespace AudioManager { + +// ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ Helper constants ‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑ +constexpr REFERENCE_TIME kTargetBufferDuration100ns = 2'000'000; // 200 ms +constexpr REFERENCE_TIME kMinSleepUs = 1'000; // 1 ms +constexpr double kDriftPositiveThresholdMs = 15.0; // audio ahead → wait +constexpr double kDriftNegativeThresholdMs = -50.0; // audio behind → drop + +// ------------------------------------------------------------------------------------ +// InitWASAPI – initialises the shared WASAPI client for the default render endpoint +// ------------------------------------------------------------------------------------ +HRESULT InitWASAPI(VideoPlayerInstance* inst, const WAVEFORMATEX* srcFmt) +{ + if (!inst) return E_INVALIDARG; + + // Re‑use previously initialised client if still valid + if (inst->pAudioClient && inst->pRenderClient) { + inst->bAudioInitialized = TRUE; + return S_OK; + } + + HRESULT hr = S_OK; + WAVEFORMATEX* deviceMixFmt = nullptr; + + // 1. Get the default render device + IMMDeviceEnumerator* enumerator = MediaFoundation::GetDeviceEnumerator(); + if (!enumerator) return E_FAIL; + + hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &inst->pDevice); + if (FAILED(hr)) return hr; + + // 2. Activate IAudioClient + IAudioEndpointVolume + hr = inst->pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, + reinterpret_cast(&inst->pAudioClient)); + if (FAILED(hr)) return hr; + + hr = inst->pDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, + reinterpret_cast(&inst->pAudioEndpointVolume)); + if (FAILED(hr)) return hr; + + // 3. Determine the format that will be rendered + if (!srcFmt) { + hr = inst->pAudioClient->GetMixFormat(&deviceMixFmt); + if (FAILED(hr)) return hr; + srcFmt = deviceMixFmt; // use mix format as fall‑back + } + inst->pSourceAudioFormat = reinterpret_cast(CoTaskMemAlloc(srcFmt->cbSize + sizeof(WAVEFORMATEX))); + memcpy(inst->pSourceAudioFormat, srcFmt, srcFmt->cbSize + sizeof(WAVEFORMATEX)); + + // 4. Create (or re‑use) the render‑ready event + if (!inst->hAudioSamplesReadyEvent) { + inst->hAudioSamplesReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!inst->hAudioSamplesReadyEvent) { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto cleanup; + } + } + + // 5. Initialise the audio client in shared, event‑callback mode + hr = inst->pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + kTargetBufferDuration100ns, // buffer dur + 0, // periodicity → let system decide + srcFmt, + nullptr); + if (FAILED(hr)) goto cleanup; + + hr = inst->pAudioClient->SetEventHandle(inst->hAudioSamplesReadyEvent); + if (FAILED(hr)) goto cleanup; + + // 6. Grab the render‑client service interface + hr = inst->pAudioClient->GetService(__uuidof(IAudioRenderClient), + reinterpret_cast(&inst->pRenderClient)); + if (FAILED(hr)) goto cleanup; + + inst->bAudioInitialized = TRUE; + +cleanup: + if (deviceMixFmt) CoTaskMemFree(deviceMixFmt); + return hr; +} + +// ---------------------------------------------------------------------------- +// AudioThreadProc – feeds decoded audio samples into the WASAPI render client +// ---------------------------------------------------------------------------- +DWORD WINAPI AudioThreadProc(LPVOID lpParam) +{ + auto* inst = static_cast(lpParam); + if (!inst || !inst->pAudioClient || !inst->pRenderClient || !inst->pSourceReaderAudio) + return 0; + + // Pre‑warm the audio engine so that GetBufferSize() is valid + UINT32 engineBufferFrames = 0; + if (FAILED(inst->pAudioClient->GetBufferSize(&engineBufferFrames))) + return 0; + + if (inst->hAudioReadyEvent) + WaitForSingleObject(inst->hAudioReadyEvent, INFINITE); + + const UINT32 blockAlign = inst->pSourceAudioFormat ? inst->pSourceAudioFormat->nBlockAlign : 4; + + // Main render loop – wait for "ready" event, then push as many frames as possible + while (inst->bAudioThreadRunning) { + DWORD signalled = WaitForSingleObject(inst->hAudioSamplesReadyEvent, 10); + if (signalled != WAIT_OBJECT_0) continue; // timeout ⇒ loop back + + // Handle seek / pause concurrently with the decoder thread + { + EnterCriticalSection(&inst->csClockSync); + bool suspended = inst->bSeekInProgress || inst->llPauseStart != 0; + LeaveCriticalSection(&inst->csClockSync); + if (suspended) { + PreciseSleepHighRes(5); + continue; + } + } + + // How many frames are currently available for writing? + UINT32 framesPadding = 0; + if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) + break; + UINT32 framesFree = engineBufferFrames - framesPadding; + if (framesFree == 0) continue; // buffer full – wait for next event + + // Read one decoded sample from MF (non‑blocking) + IMFSample* sample = nullptr; + DWORD flags = 0; + LONGLONG ts100n = 0; + HRESULT hr = inst->pSourceReaderAudio->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + 0, nullptr, &flags, &ts100n, &sample); + if (FAILED(hr)) break; + if (!sample) continue; // decoder starved – wait for more data + if (flags & MF_SOURCE_READERF_ENDOFSTREAM) { + sample->Release(); + break; + } + + // Measure drift between sample PTS and wall clock (real elapsed time) + // This ensures audio and video are synchronized to the same time reference + double driftMs = 0.0; + if (inst->bUseClockSync && inst->llPlaybackStartTime != 0 && ts100n > 0) { + // Calculate elapsed time since playback started (in milliseconds) + LONGLONG currentTimeMs = GetCurrentTimeMs(); + LONGLONG elapsedMs = currentTimeMs - inst->llPlaybackStartTime - inst->llTotalPauseTime; + + // Apply playback speed to elapsed time + double adjustedElapsedMs = elapsedMs * inst->playbackSpeed; + + // Convert sample timestamp from 100ns units to milliseconds + double sampleTimeMs = ts100n / 10000.0; + + // Calculate drift: positive means audio is ahead, negative means audio is late + driftMs = sampleTimeMs - adjustedElapsedMs; + } + + if (driftMs > kDriftPositiveThresholdMs) { + // Audio ahead → delay feed to renderer + PreciseSleepHighRes(std::min(driftMs, 100.0)); + } else if (driftMs < kDriftNegativeThresholdMs) { + // Audio too late → drop sample completely (skip) + sample->Release(); + continue; + } + + // Copy contiguous audio buffer into render buffer – may span multiple GetBuffer() calls + IMFMediaBuffer* mediaBuf = nullptr; + if (FAILED(sample->ConvertToContiguousBuffer(&mediaBuf)) || !mediaBuf) { + sample->Release(); + continue; + } + + BYTE* srcData = nullptr; + DWORD srcSize = 0, srcMax = 0; + if (FAILED(mediaBuf->Lock(&srcData, &srcMax, &srcSize))) { + mediaBuf->Release(); + sample->Release(); + continue; + } + + UINT32 totalFrames = srcSize / blockAlign; + UINT32 offsetFrames = 0; + + while (offsetFrames < totalFrames) { + UINT32 framesWanted = std::min(totalFrames - offsetFrames, framesFree); + if (framesWanted == 0) { + // Renderer is full → wait for next event + WaitForSingleObject(inst->hAudioSamplesReadyEvent, 5); + if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) break; + framesFree = engineBufferFrames - framesPadding; + continue; + } + + BYTE* dstData = nullptr; + if (FAILED(inst->pRenderClient->GetBuffer(framesWanted, &dstData)) || !dstData) break; + + const BYTE* chunkStart = srcData + (offsetFrames * blockAlign); + memcpy(dstData, chunkStart, framesWanted * blockAlign); + + // Apply per‑instance volume in‑place (16‑bit PCM or IEEE‑float) + if (inst->instanceVolume < 0.999f) { + if (inst->pSourceAudioFormat->wFormatTag == WAVE_FORMAT_PCM && + inst->pSourceAudioFormat->wBitsPerSample == 16) { + auto* s = reinterpret_cast(dstData); + size_t n = (framesWanted * blockAlign) / sizeof(int16_t); + for (size_t i = 0; i < n; ++i) s[i] = static_cast(s[i] * inst->instanceVolume); + } else if (inst->pSourceAudioFormat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT && + inst->pSourceAudioFormat->wBitsPerSample == 32) { + auto* s = reinterpret_cast(dstData); + size_t n = (framesWanted * blockAlign) / sizeof(float); + for (size_t i = 0; i < n; ++i) s[i] *= inst->instanceVolume; + } + } + + inst->pRenderClient->ReleaseBuffer(framesWanted, 0); + offsetFrames += framesWanted; + + // Recompute free frames for potential second iteration in this loop + if (FAILED(inst->pAudioClient->GetCurrentPadding(&framesPadding))) break; + framesFree = engineBufferFrames - framesPadding; + } + + mediaBuf->Unlock(); + mediaBuf->Release(); + sample->Release(); + } + + inst->pAudioClient->Stop(); + return 0; +} + +// ------------------------------------------------------------- +// Thread management helpers +// ------------------------------------------------------------- +HRESULT StartAudioThread(VideoPlayerInstance* inst) +{ + if (!inst || !inst->bHasAudio || !inst->bAudioInitialized) + return E_INVALIDARG; + + // Terminate any previous thread first + if (inst->hAudioThread) { + WaitForSingleObject(inst->hAudioThread, 5000); + CloseHandle(inst->hAudioThread); + inst->hAudioThread = nullptr; + } + + inst->bAudioThreadRunning = TRUE; + inst->hAudioThread = CreateThread(nullptr, 0, AudioThreadProc, inst, 0, nullptr); + if (!inst->hAudioThread) { + inst->bAudioThreadRunning = FALSE; + return HRESULT_FROM_WIN32(GetLastError()); + } + + if (inst->hAudioReadyEvent) SetEvent(inst->hAudioReadyEvent); + return S_OK; +} + +void StopAudioThread(VideoPlayerInstance* inst) +{ + if (!inst) return; + + inst->bAudioThreadRunning = FALSE; + if (inst->hAudioThread) { + if (WaitForSingleObject(inst->hAudioThread, 1000) == WAIT_TIMEOUT) + TerminateThread(inst->hAudioThread, 0); + CloseHandle(inst->hAudioThread); + inst->hAudioThread = nullptr; + } + + if (inst->pAudioClient) inst->pAudioClient->Stop(); +} + +// ----------------------------------------- +// Per‑instance volume helpers (0.0 – 1.0) +// ----------------------------------------- +HRESULT SetVolume(VideoPlayerInstance* inst, float vol) +{ + if (!inst) return E_INVALIDARG; + inst->instanceVolume = std::clamp(vol, 0.0f, 1.0f); + return S_OK; +} + +HRESULT GetVolume(const VideoPlayerInstance* inst, float* out) +{ + if (!inst || !out) return E_INVALIDARG; + *out = inst->instanceVolume; + return S_OK; +} + +// ------------------------------------------- +// Peak‑meter (endpoint) level in percentage +// ------------------------------------------- +HRESULT GetAudioLevels(const VideoPlayerInstance* inst, float* left, float* right) +{ + if (!inst || !left || !right) return E_INVALIDARG; + if (!inst->pDevice) return E_FAIL; + + IAudioMeterInformation* meter = nullptr; + HRESULT hr = inst->pDevice->Activate(__uuidof(IAudioMeterInformation), CLSCTX_ALL, nullptr, + reinterpret_cast(&meter)); + if (FAILED(hr)) return hr; + + std::array peaks = {0.f, 0.f}; + hr = meter->GetChannelsPeakValues(2, peaks.data()); + meter->Release(); + if (FAILED(hr)) return hr; + + auto toPercent = [](float level) { + if (level <= 0.f) return 0.f; + float db = 20.f * log10(level); + float pct = std::clamp((db + 60.f) / 60.f, 0.f, 1.f); + return pct * 100.f; + }; + + *left = toPercent(peaks[0]); + *right = toPercent(peaks[1]); + return S_OK; +} + +} // namespace AudioManager diff --git a/mediaplayer/src/jvmMain/native/windows/AudioManager.h b/mediaplayer/src/jvmMain/native/windows/AudioManager.h new file mode 100644 index 00000000..0b17eee1 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/AudioManager.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Error code definitions +#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) +#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) +#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) + +// Forward declarations +struct VideoPlayerInstance; + +namespace AudioManager { + +/** + * @brief Initializes WASAPI for audio playback. + * @param pInstance Pointer to the video player instance. + * @param pSourceFormat Optional source audio format. + * @return S_OK on success, or an error code. + */ +HRESULT InitWASAPI(VideoPlayerInstance* pInstance, const WAVEFORMATEX* pSourceFormat = nullptr); + +/** + * @brief Audio processing thread procedure. + * @param lpParam Pointer to the video player instance. + * @return Thread exit code. + */ +DWORD WINAPI AudioThreadProc(LPVOID lpParam); + +/** + * @brief Starts the audio thread for a video player instance. + * @param pInstance Pointer to the video player instance. + * @return S_OK on success, or an error code. + */ +HRESULT StartAudioThread(VideoPlayerInstance* pInstance); + +/** + * @brief Stops the audio thread for a video player instance. + * @param pInstance Pointer to the video player instance. + */ +void StopAudioThread(VideoPlayerInstance* pInstance); + +/** + * @brief Sets the audio volume for a video player instance. + * @param pInstance Pointer to the video player instance. + * @param volume Volume level (0.0 to 1.0). + * @return S_OK on success, or an error code. + */ +HRESULT SetVolume(VideoPlayerInstance* pInstance, float volume); + +/** + * @brief Gets the audio volume for a video player instance. + * @param pInstance Pointer to the video player instance. + * @param volume Pointer to receive the volume level. + * @return S_OK on success, or an error code. + */ +HRESULT GetVolume(const VideoPlayerInstance* pInstance, float* volume); + +/** + * @brief Gets the audio levels for a video player instance. + * @param pInstance Pointer to the video player instance. + * @param pLeftLevel Pointer to receive the left channel level. + * @param pRightLevel Pointer to receive the right channel level. + * @return S_OK on success, or an error code. + */ +HRESULT GetAudioLevels(const VideoPlayerInstance* pInstance, float* pLeftLevel, float* pRightLevel); + +} // namespace AudioManager diff --git a/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt new file mode 100644 index 00000000..a036a3ac --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.15) +project(NativeVideoPlayer LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) + +# Check target architecture +if(CMAKE_GENERATOR_PLATFORM STREQUAL "x64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "") + set(TARGET_ARCH "x64") + set(OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../resources/win32-x86-64") + add_compile_options("/arch:AVX2") +elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") + set(TARGET_ARCH "ARM64") + set(OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../resources/win32-arm64") + add_compile_options("/arch:arm64") +else() + message(FATAL_ERROR "Unsupported architecture: ${CMAKE_GENERATOR_PLATFORM}") +endif() + +# Ensure output directory exists +file(MAKE_DIRECTORY ${OUTPUT_DIR}) + +# Define the target +add_library(NativeVideoPlayer SHARED + NativeVideoPlayer.cpp + NativeVideoPlayer.h + VideoPlayerInstance.h + Utils.cpp + Utils.h + MediaFoundationManager.cpp + MediaFoundationManager.h + AudioManager.cpp + AudioManager.h +) + +# Compilation definitions +target_compile_definitions(NativeVideoPlayer PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX + NATIVEVIDEOPLAYER_EXPORTS +) + +# Linked libraries +target_link_libraries(NativeVideoPlayer PRIVATE + mf + mfplat + mfreadwrite + mfuuid + wmcodecdspuuid + ole32 + oleaut32 + avrt + mfsensorgroup + dxva2 + d3d11 + dxgi + evr +) + +# Configure output directory +set_target_properties(NativeVideoPlayer PROPERTIES + OUTPUT_NAME "NativeVideoPlayer" + LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIR}" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_DIR}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIR}" +) + +# Display target architecture and output directory +message(STATUS "Target architecture: ${TARGET_ARCH}") +message(STATUS "Output directory: ${OUTPUT_DIR}") diff --git a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp new file mode 100644 index 00000000..74e1f933 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.cpp @@ -0,0 +1,130 @@ +#include "MediaFoundationManager.h" +#include +#include +#include + +namespace MediaFoundation { + +// Global resources shared across all instances +static bool g_bMFInitialized = false; +static ID3D11Device* g_pD3DDevice = nullptr; +static IMFDXGIDeviceManager* g_pDXGIDeviceManager = nullptr; +static UINT32 g_dwResetToken = 0; +static IMMDeviceEnumerator* g_pEnumerator = nullptr; +static int g_instanceCount = 0; + +HRESULT Initialize() { + if (g_bMFInitialized) + return OP_E_ALREADY_INITIALIZED; + + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (SUCCEEDED(hr)) + hr = MFStartup(MF_VERSION); + if (FAILED(hr)) + return hr; + + hr = CreateDX11Device(); + if (FAILED(hr)) { + MFShutdown(); + return hr; + } + + hr = MFCreateDXGIDeviceManager(&g_dwResetToken, &g_pDXGIDeviceManager); + if (SUCCEEDED(hr)) + hr = g_pDXGIDeviceManager->ResetDevice(g_pD3DDevice, g_dwResetToken); + if (FAILED(hr)) { + if (g_pD3DDevice) { + g_pD3DDevice->Release(); + g_pD3DDevice = nullptr; + } + MFShutdown(); + return hr; + } + + g_bMFInitialized = true; + return S_OK; +} + +HRESULT Shutdown() { + if (g_instanceCount > 0) + return E_FAIL; // Instances still active + + HRESULT hr = S_OK; + + // Release DXGI and D3D resources + if (g_pDXGIDeviceManager) { + g_pDXGIDeviceManager->Release(); + g_pDXGIDeviceManager = nullptr; + } + + if (g_pD3DDevice) { + g_pD3DDevice->Release(); + g_pD3DDevice = nullptr; + } + + // Release audio enumerator + if (g_pEnumerator) { + g_pEnumerator->Release(); + g_pEnumerator = nullptr; + } + + // Shutdown Media Foundation last + if (g_bMFInitialized) { + hr = MFShutdown(); + g_bMFInitialized = false; + } + + // Uninitialize COM + CoUninitialize(); + return hr; +} + +HRESULT CreateDX11Device() { + HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, nullptr, 0, + D3D11_SDK_VERSION, &g_pD3DDevice, nullptr, nullptr); + if (FAILED(hr)) + return hr; + + ID3D10Multithread* pMultithread = nullptr; + if (SUCCEEDED(g_pD3DDevice->QueryInterface(__uuidof(ID3D10Multithread), reinterpret_cast(&pMultithread)))) { + pMultithread->SetMultithreadProtected(TRUE); + pMultithread->Release(); + } + + return hr; +} + +ID3D11Device* GetD3DDevice() { + return g_pD3DDevice; +} + +IMFDXGIDeviceManager* GetDXGIDeviceManager() { + return g_pDXGIDeviceManager; +} + +IMMDeviceEnumerator* GetDeviceEnumerator() { + if (!g_pEnumerator) { + CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, + IID_PPV_ARGS(&g_pEnumerator)); + } + return g_pEnumerator; +} + +void IncrementInstanceCount() { + g_instanceCount++; +} + +void DecrementInstanceCount() { + g_instanceCount--; +} + +bool IsInitialized() { + return g_bMFInitialized; +} + +int GetInstanceCount() { + return g_instanceCount; +} + +} // namespace MediaFoundation \ No newline at end of file diff --git a/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h new file mode 100644 index 00000000..495e04eb --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/MediaFoundationManager.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Error code definitions +#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) +#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) +#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) + +namespace MediaFoundation { + +/** + * @brief Initializes Media Foundation, Direct3D11, and the DXGI device manager. + * @return S_OK on success, or an error code. + */ +HRESULT Initialize(); + +/** + * @brief Shuts down Media Foundation and releases global resources. + * @return S_OK on success, or an error code. + */ +HRESULT Shutdown(); + +/** + * @brief Creates a Direct3D11 device with video support. + * @return S_OK on success, or an error code. + */ +HRESULT CreateDX11Device(); + +/** + * @brief Gets the D3D11 device. + * @return Pointer to the D3D11 device. + */ +ID3D11Device* GetD3DDevice(); + +/** + * @brief Gets the DXGI device manager. + * @return Pointer to the DXGI device manager. + */ +IMFDXGIDeviceManager* GetDXGIDeviceManager(); + +/** + * @brief Gets the device enumerator for audio devices. + * @return Pointer to the device enumerator. + */ +IMMDeviceEnumerator* GetDeviceEnumerator(); + +/** + * @brief Increments the instance count. + */ +void IncrementInstanceCount(); + +/** + * @brief Decrements the instance count. + */ +void DecrementInstanceCount(); + +/** + * @brief Checks if Media Foundation is initialized. + * @return True if initialized, false otherwise. + */ +bool IsInitialized(); + +/** + * @brief Gets the current instance count. + * @return The number of active instances. + */ +int GetInstanceCount(); + +} // namespace MediaFoundation diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp new file mode 100644 index 00000000..893e5cba --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.cpp @@ -0,0 +1,1349 @@ +// NativeVideoPlayer.cpp +#include "NativeVideoPlayer.h" +#include "VideoPlayerInstance.h" +#include "Utils.h" +#include "MediaFoundationManager.h" +#include "AudioManager.h" +#include +#include +#include +#include + +// For IMF2DBuffer and IMF2DBuffer2 interfaces +#include + +using namespace VideoPlayerUtils; +using namespace MediaFoundation; +using namespace AudioManager; + +// Error code definitions from header +#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) +#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) +#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) + +// Debug print macro +#ifdef _DEBUG +#define PrintHR(msg, hr) fprintf(stderr, "%s (hr=0x%08x)\n", msg, static_cast(hr)) +#else +#define PrintHR(msg, hr) ((void)0) +#endif + +// API Implementation +NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation() { + return Initialize(); +} + +NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** ppInstance) { + // Parameter validation + if (!ppInstance) + return E_INVALIDARG; + + // Ensure Media Foundation is initialized + if (!IsInitialized()) { + HRESULT hr = Initialize(); + if (FAILED(hr)) + return hr; + } + + // Allocate and initialize a new instance + auto* pInstance = new (std::nothrow) VideoPlayerInstance(); + if (!pInstance) + return E_OUTOFMEMORY; + + // Initialize critical section for synchronization + InitializeCriticalSection(&pInstance->csClockSync); + + pInstance->bUseClockSync = TRUE; + + // Create audio synchronization event + pInstance->hAudioReadyEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!pInstance->hAudioReadyEvent) { + DeleteCriticalSection(&pInstance->csClockSync); + delete pInstance; + return HRESULT_FROM_WIN32(GetLastError()); + } + + // Increment instance count and return the instance + IncrementInstanceCount(); + *ppInstance = pInstance; + return S_OK; +} + +NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance) { + if (pInstance) { + // Ensure all media resources are released + CloseMedia(pInstance); + + // Double-check that cached sample is released + // This is already done in CloseMedia, but we do it again as a safety measure + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + + // Delete critical section + DeleteCriticalSection(&pInstance->csClockSync); + + // Delete instance and decrement counter + delete pInstance; + DecrementInstanceCount(); + } +} + +NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback) { + // Parameter validation + if (!pInstance || !url) + return OP_E_INVALID_PARAMETER; + if (!IsInitialized()) + return OP_E_NOT_INITIALIZED; + + // Close previous media and reset state + CloseMedia(pInstance); + pInstance->bEOF = FALSE; + pInstance->videoWidth = pInstance->videoHeight = 0; + pInstance->bHasAudio = FALSE; + + // Initialize frame caching for paused state + pInstance->bHasInitialFrame = FALSE; + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + + HRESULT hr = S_OK; + + // Helper function to safely release COM objects + auto safeRelease = [](IUnknown* obj) { if (obj) obj->Release(); }; + + // 1. Configure and open media source with both audio and video streams + // ------------------------------------------------------------------ + IMFAttributes* pAttributes = nullptr; + hr = MFCreateAttributes(&pAttributes, 5); + if (FAILED(hr)) + return hr; + + // Configure attributes for hardware acceleration + pAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); + pAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE); + pAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, GetDXGIDeviceManager()); + + // Enable advanced video processing for better synchronization + pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE); + + // Create source reader for both audio and video + hr = MFCreateSourceReaderFromURL(url, pAttributes, &pInstance->pSourceReader); + safeRelease(pAttributes); + if (FAILED(hr)) + return hr; + + // 2. Configure video stream + // ------------------------------------------ + // Enable video stream + hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); + if (SUCCEEDED(hr)) + hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE); + if (FAILED(hr)) + return hr; + + // Configure video format (RGB32) + IMFMediaType* pType = nullptr; + hr = MFCreateMediaType(&pType); + if (SUCCEEDED(hr)) { + hr = pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + if (SUCCEEDED(hr)) + hr = pType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + if (SUCCEEDED(hr)) + hr = pInstance->pSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, pType); + safeRelease(pType); + } + if (FAILED(hr)) + return hr; + + // Get video dimensions + IMFMediaType* pCurrent = nullptr; + hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pCurrent); + if (SUCCEEDED(hr)) { + hr = MFGetAttributeSize(pCurrent, MF_MT_FRAME_SIZE, &pInstance->videoWidth, &pInstance->videoHeight); + safeRelease(pCurrent); + } + + // 3. Configure audio stream (if available) + // ------------------------------------------ + // Try to enable audio stream + hr = pInstance->pSourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); + if (SUCCEEDED(hr)) { + // Configure audio format (PCM 16-bit stereo 48kHz) + IMFMediaType* pWantedType = nullptr; + hr = MFCreateMediaType(&pWantedType); + if (SUCCEEDED(hr)) { + pWantedType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + pWantedType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); + pWantedType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); + pWantedType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 48000); + pWantedType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); + pWantedType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 192000); + pWantedType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); + hr = pInstance->pSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedType); + safeRelease(pWantedType); + } + + if (SUCCEEDED(hr)) { + // Get the actual audio format for WASAPI + IMFMediaType* pActualType = nullptr; + hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &pActualType); + if (SUCCEEDED(hr) && pActualType) { + WAVEFORMATEX* pWfx = nullptr; + UINT32 size = 0; + hr = MFCreateWaveFormatExFromMFMediaType(pActualType, &pWfx, &size); + if (SUCCEEDED(hr) && pWfx) { + hr = InitWASAPI(pInstance, pWfx); + if (FAILED(hr)) { + PrintHR("InitWASAPI failed", hr); + if (pWfx) CoTaskMemFree(pWfx); + safeRelease(pActualType); + } else { + if (pInstance->pSourceAudioFormat) + CoTaskMemFree(pInstance->pSourceAudioFormat); + pInstance->pSourceAudioFormat = pWfx; + pInstance->bHasAudio = TRUE; + } + } + safeRelease(pActualType); + } + } + + // Create a separate audio source reader for the audio thread + // This is needed even with automatic synchronization + hr = MFCreateSourceReaderFromURL(url, nullptr, &pInstance->pSourceReaderAudio); + if (SUCCEEDED(hr)) { + // Select only audio stream + hr = pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); + if (SUCCEEDED(hr)) + hr = pInstance->pSourceReaderAudio->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); + + if (SUCCEEDED(hr)) { + // Configure audio format (same as main reader) + IMFMediaType* pWantedAudioType = nullptr; + hr = MFCreateMediaType(&pWantedAudioType); + if (SUCCEEDED(hr)) { + pWantedAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + pWantedAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); + pWantedAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2); + pWantedAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, 48000); + pWantedAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4); + pWantedAudioType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 192000); + pWantedAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16); + hr = pInstance->pSourceReaderAudio->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pWantedAudioType); + safeRelease(pWantedAudioType); + } + } + + if (FAILED(hr)) { + PrintHR("Failed to configure audio source reader", hr); + safeRelease(pInstance->pSourceReaderAudio); + pInstance->pSourceReaderAudio = nullptr; + } + } else { + PrintHR("Failed to create audio source reader", hr); + } + } + + if (pInstance->bUseClockSync) { + // 4. Set up presentation clock for synchronization + // ---------------------------------------------------------- + // Get the media source from the source reader + hr = pInstance->pSourceReader->GetServiceForStream( + MF_SOURCE_READER_MEDIASOURCE, + GUID_NULL, + IID_PPV_ARGS(&pInstance->pMediaSource)); + + if (SUCCEEDED(hr)) { + // Create the presentation clock + hr = MFCreatePresentationClock(&pInstance->pPresentationClock); + if (SUCCEEDED(hr)) { + // Create a system time source + IMFPresentationTimeSource* pTimeSource = nullptr; + hr = MFCreateSystemTimeSource(&pTimeSource); + if (SUCCEEDED(hr)) { + // Set the time source on the presentation clock + hr = pInstance->pPresentationClock->SetTimeSource(pTimeSource); + if (SUCCEEDED(hr)) { + // Set the rate control on the presentation clock + IMFRateControl* pRateControl = nullptr; + hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); + if (SUCCEEDED(hr)) { + // Explicitly set rate to 1.0 to ensure correct initial playback speed + hr = pRateControl->SetRate(FALSE, 1.0f); + if (FAILED(hr)) { + PrintHR("Failed to set initial presentation clock rate", hr); + } + pRateControl->Release(); + } + + // Get the media sink from the media source + IMFMediaSink* pMediaSink = nullptr; + hr = pInstance->pMediaSource->QueryInterface(IID_PPV_ARGS(&pMediaSink)); + if (SUCCEEDED(hr)) { + // Set the presentation clock on the media sink + IMFClockStateSink* pClockStateSink = nullptr; + hr = pMediaSink->QueryInterface(IID_PPV_ARGS(&pClockStateSink)); + if (SUCCEEDED(hr)) { + // Start the presentation clock only if startPlayback is TRUE + // This allows the player to be initialized in a paused state + // when InitialPlayerState.PAUSE is specified in the Kotlin code + if (startPlayback) { + hr = pInstance->pPresentationClock->Start(0); + if (FAILED(hr)) { + PrintHR("Failed to start presentation clock", hr); + } + } else { + // If not starting playback, initialize the clock but don't start it + // This keeps the player in a paused state until explicitly started + hr = pInstance->pPresentationClock->Pause(); + if (FAILED(hr)) { + PrintHR("Failed to pause presentation clock", hr); + // Continue even if pause fails - this is not a critical error + // The player will still be usable, just not in the ideal initial state + } + } + pClockStateSink->Release(); + } + pMediaSink->Release(); + } else { + PrintHR("Failed to get media sink from media source", hr); + } + } + safeRelease(pTimeSource); + } + } + } + } + + // 5. Initialize playback timing and start audio thread + // ---------------------------------------------------- + if (startPlayback) { + // IMPORTANT: Initialize llPlaybackStartTime when starting playback + // This is crucial for A/V synchronization - without this, the sync code won't work + pInstance->llPlaybackStartTime = GetCurrentTimeMs(); + pInstance->llTotalPauseTime = 0; + pInstance->llPauseStart = 0; + + // Start audio thread if audio is available + if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { + hr = StartAudioThread(pInstance); + if (FAILED(hr)) { + PrintHR("StartAudioThread failed", hr); + } + } + } + + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYTE** pData, DWORD* pDataSize) { + if (!pInstance || !pInstance->pSourceReader || !pData || !pDataSize) + return OP_E_NOT_INITIALIZED; + + if (pInstance->pLockedBuffer) + UnlockVideoFrame(pInstance); + + if (pInstance->bEOF) { + *pData = nullptr; + *pDataSize = 0; + return S_FALSE; + } + + // Check if player is paused + BOOL isPaused = (pInstance->llPauseStart != 0); + IMFSample* pSample = nullptr; + HRESULT hr = S_OK; + DWORD streamIndex = 0, dwFlags = 0; + LONGLONG llTimestamp = 0; + + if (isPaused) { + // Player is paused - check if we need to read an initial frame + if (!pInstance->bHasInitialFrame) { + // Read one frame when paused and cache it + hr = pInstance->pSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &dwFlags, &llTimestamp, &pSample); + if (FAILED(hr)) + return hr; + + if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + pInstance->bEOF = TRUE; + if (pSample) pSample->Release(); + *pData = nullptr; + *pDataSize = 0; + return S_FALSE; + } + + if (!pSample) { + *pData = nullptr; + *pDataSize = 0; + return S_OK; + } + + // Store the frame for future use + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + pSample->AddRef(); // Add reference for the cached sample + pInstance->pCachedSample = pSample; + pInstance->bHasInitialFrame = TRUE; + + // Don't update position when paused - keep the current position + } else { + // Already have an initial frame, use the cached sample + if (pInstance->pCachedSample) { + pSample = pInstance->pCachedSample; + pSample->AddRef(); // Add reference for this function's use + // Don't update position when paused + } else { + // No cached sample available (shouldn't happen) + *pData = nullptr; + *pDataSize = 0; + return S_OK; + } + } + } else { + // Player is playing - read a new frame + hr = pInstance->pSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &dwFlags, &llTimestamp, &pSample); + if (FAILED(hr)) + return hr; + + if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + pInstance->bEOF = TRUE; + if (pSample) pSample->Release(); + *pData = nullptr; + *pDataSize = 0; + return S_FALSE; + } + + if (!pSample) { + *pData = nullptr; + *pDataSize = 0; + return S_OK; + } + + // Update cached sample for future paused state + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + pSample->AddRef(); // Add reference for the cached sample + pInstance->pCachedSample = pSample; + + // Store current position when playing + pInstance->llCurrentPosition = llTimestamp; + } + + // Synchronization using wall clock time (real elapsed time since playback started) + // This is more reliable than the presentation clock which is not tied to the source reader + if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0 && llTimestamp > 0) { + // Calculate elapsed time since playback started (in milliseconds) + LONGLONG currentTimeMs = GetCurrentTimeMs(); + LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; + + // Apply playback speed to elapsed time + double adjustedElapsedMs = elapsedMs * pInstance->playbackSpeed; + + // Convert frame timestamp from 100ns units to milliseconds + double frameTimeMs_ts = llTimestamp / 10000.0; + + // Calculate frame rate for skip threshold + UINT frameRateNum = 60, frameRateDenom = 1; + GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); + double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; + + // Calculate difference: positive means frame is ahead, negative means frame is late + double diffMs = frameTimeMs_ts - adjustedElapsedMs; + + // If frame is very late (more than 3 frames behind), skip it + if (diffMs < -frameIntervalMs * 3) { + pSample->Release(); + *pData = nullptr; + *pDataSize = 0; + return S_OK; + } + // If frame is ahead of schedule, wait to maintain correct frame rate + else if (diffMs > 1.0) { + // Limit maximum wait time to avoid freezing if timestamps are far apart + double waitTime = std::min(diffMs, frameIntervalMs * 2); + PreciseSleepHighRes(waitTime); + } + } + + IMFMediaBuffer* pBuffer = nullptr; + DWORD bufferCount = 0; + hr = pSample->GetBufferCount(&bufferCount); + if (SUCCEEDED(hr) && bufferCount == 1) { + hr = pSample->GetBufferByIndex(0, &pBuffer); + } else { + hr = pSample->ConvertToContiguousBuffer(&pBuffer); + } + if (FAILED(hr)) { + PrintHR("Failed to get contiguous buffer", hr); + pSample->Release(); + return hr; + } + + BYTE* pBytes = nullptr; + DWORD cbMax = 0, cbCurr = 0; + hr = pBuffer->Lock(&pBytes, &cbMax, &cbCurr); + if (FAILED(hr)) { + PrintHR("Buffer->Lock failed", hr); + pBuffer->Release(); + pSample->Release(); + return hr; + } + + pInstance->pLockedBuffer = pBuffer; + pInstance->pLockedBytes = pBytes; + pInstance->lockedMaxSize = cbMax; + pInstance->lockedCurrSize = cbCurr; + *pData = pBytes; + *pDataSize = cbCurr; + pSample->Release(); + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance) { + if (!pInstance) + return E_INVALIDARG; + if (pInstance->pLockedBuffer) { + pInstance->pLockedBuffer->Unlock(); + pInstance->pLockedBuffer->Release(); + pInstance->pLockedBuffer = nullptr; + } + pInstance->pLockedBytes = nullptr; + pInstance->lockedMaxSize = pInstance->lockedCurrSize = 0; + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( + VideoPlayerInstance* pInstance, + BYTE* pDst, + DWORD dstRowBytes, + DWORD dstCapacity, + LONGLONG* pTimestamp) { + if (!pInstance || !pDst || dstRowBytes == 0 || dstCapacity == 0) { + return OP_E_INVALID_PARAMETER; + } + + if (!pInstance->pSourceReader) + return OP_E_NOT_INITIALIZED; + + if (pInstance->pLockedBuffer) + UnlockVideoFrame(pInstance); + + if (pInstance->bEOF) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_FALSE; + } + + // Check if player is paused + BOOL isPaused = (pInstance->llPauseStart != 0); + IMFSample* pSample = nullptr; + HRESULT hr = S_OK; + DWORD streamIndex = 0, dwFlags = 0; + LONGLONG llTimestamp = 0; + + if (isPaused) { + if (!pInstance->bHasInitialFrame) { + hr = pInstance->pSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &dwFlags, &llTimestamp, &pSample); + if (FAILED(hr)) return hr; + + if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + pInstance->bEOF = TRUE; + if (pSample) pSample->Release(); + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_FALSE; + } + + if (!pSample) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_OK; + } + + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + pSample->AddRef(); + pInstance->pCachedSample = pSample; + pInstance->bHasInitialFrame = TRUE; + } else { + if (pInstance->pCachedSample) { + pSample = pInstance->pCachedSample; + pSample->AddRef(); + } else { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_OK; + } + } + } else { + hr = pInstance->pSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &dwFlags, &llTimestamp, &pSample); + if (FAILED(hr)) return hr; + + if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + pInstance->bEOF = TRUE; + if (pSample) pSample->Release(); + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_FALSE; + } + + if (!pSample) { + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_OK; + } + + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + pSample->AddRef(); + pInstance->pCachedSample = pSample; + pInstance->llCurrentPosition = llTimestamp; + } + + // Frame timing synchronization + if (pInstance->bUseClockSync && pInstance->llPlaybackStartTime != 0 && llTimestamp > 0) { + LONGLONG currentTimeMs = GetCurrentTimeMs(); + LONGLONG elapsedMs = currentTimeMs - pInstance->llPlaybackStartTime - pInstance->llTotalPauseTime; + double adjustedElapsedMs = elapsedMs * pInstance->playbackSpeed; + double frameTimeMs_ts = llTimestamp / 10000.0; + + UINT frameRateNum = 60, frameRateDenom = 1; + GetVideoFrameRate(pInstance, &frameRateNum, &frameRateDenom); + double frameIntervalMs = 1000.0 * frameRateDenom / frameRateNum; + + double diffMs = frameTimeMs_ts - adjustedElapsedMs; + + if (diffMs < -frameIntervalMs * 3) { + pSample->Release(); + if (pTimestamp) *pTimestamp = pInstance->llCurrentPosition; + return S_OK; + } + else if (diffMs > 1.0) { + double waitTime = std::min(diffMs, frameIntervalMs * 2); + PreciseSleepHighRes(waitTime); + } + } + + if (pTimestamp) { + *pTimestamp = pInstance->llCurrentPosition; + } + + const UINT32 width = pInstance->videoWidth; + const UINT32 height = pInstance->videoHeight; + if (width == 0 || height == 0) { + pSample->Release(); + return S_FALSE; + } + + const DWORD requiredDst = dstRowBytes * height; + if (dstCapacity < requiredDst) { + pSample->Release(); + return OP_E_INVALID_PARAMETER; + } + + // Try to use IMF2DBuffer2 for optimized zero-copy access + IMFMediaBuffer* pBuffer = nullptr; + hr = pSample->ConvertToContiguousBuffer(&pBuffer); + if (FAILED(hr)) { + pSample->Release(); + return hr; + } + + // Attempt IMF2DBuffer2 for direct 2D access (most efficient) + IMF2DBuffer2* p2DBuffer2 = nullptr; + IMF2DBuffer* p2DBuffer = nullptr; + BYTE* pScanline0 = nullptr; + LONG srcPitch = 0; + BYTE* pBufferStart = nullptr; + DWORD cbBufferLength = 0; + bool usedDirect2D = false; + + hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer2)); + if (SUCCEEDED(hr) && p2DBuffer2) { + // Use Lock2DSize for optimal access - avoids internal copies + hr = p2DBuffer2->Lock2DSize(MF2DBuffer_LockFlags_Read, &pScanline0, &srcPitch, &pBufferStart, &cbBufferLength); + if (SUCCEEDED(hr)) { + usedDirect2D = true; + const DWORD srcRowBytes = width * 4; + + // Zero-copy path: if strides match exactly, use memcpy for the entire buffer + if (static_cast(dstRowBytes) == srcPitch && static_cast(srcRowBytes) == srcPitch) { + memcpy(pDst, pScanline0, srcRowBytes * height); + } else { + // Strides differ - must copy row by row but still more efficient than MFCopyImage + BYTE* pSrc = pScanline0; + BYTE* pDstRow = pDst; + const DWORD copyBytes = std::min(srcRowBytes, dstRowBytes); + for (UINT32 y = 0; y < height; y++) { + memcpy(pDstRow, pSrc, copyBytes); + pSrc += srcPitch; + pDstRow += dstRowBytes; + } + } + p2DBuffer2->Unlock2D(); + } + p2DBuffer2->Release(); + } + + // Fallback to IMF2DBuffer if IMF2DBuffer2 failed + if (!usedDirect2D) { + hr = pBuffer->QueryInterface(IID_PPV_ARGS(&p2DBuffer)); + if (SUCCEEDED(hr) && p2DBuffer) { + hr = p2DBuffer->Lock2D(&pScanline0, &srcPitch); + if (SUCCEEDED(hr)) { + usedDirect2D = true; + const DWORD srcRowBytes = width * 4; + + if (static_cast(dstRowBytes) == srcPitch && static_cast(srcRowBytes) == srcPitch) { + memcpy(pDst, pScanline0, srcRowBytes * height); + } else { + BYTE* pSrc = pScanline0; + BYTE* pDstRow = pDst; + const DWORD copyBytes = std::min(srcRowBytes, dstRowBytes); + for (UINT32 y = 0; y < height; y++) { + memcpy(pDstRow, pSrc, copyBytes); + pSrc += srcPitch; + pDstRow += dstRowBytes; + } + } + p2DBuffer->Unlock2D(); + } + p2DBuffer->Release(); + } + } + + // Ultimate fallback to standard buffer lock + if (!usedDirect2D) { + BYTE* pBytes = nullptr; + DWORD cbMax = 0, cbCurr = 0; + hr = pBuffer->Lock(&pBytes, &cbMax, &cbCurr); + if (SUCCEEDED(hr)) { + const DWORD srcRowBytes = width * 4; + const DWORD requiredSrc = srcRowBytes * height; + if (cbCurr >= requiredSrc) { + // Use MFCopyImage as last resort + MFCopyImage(pDst, dstRowBytes, pBytes, srcRowBytes, srcRowBytes, height); + } + pBuffer->Unlock(); + } + } + + pBuffer->Release(); + pSample->Release(); + return S_OK; +} + +NATIVEVIDEOPLAYER_API BOOL IsEOF(const VideoPlayerInstance* pInstance) { + if (!pInstance) + return FALSE; + return pInstance->bEOF; +} + +NATIVEVIDEOPLAYER_API void GetVideoSize(const VideoPlayerInstance* pInstance, UINT32* pWidth, UINT32* pHeight) { + if (!pInstance) + return; + if (pWidth) *pWidth = pInstance->videoWidth; + if (pHeight) *pHeight = pInstance->videoHeight; +} + +NATIVEVIDEOPLAYER_API HRESULT GetVideoFrameRate(const VideoPlayerInstance* pInstance, UINT* pNum, UINT* pDenom) { + if (!pInstance || !pInstance->pSourceReader || !pNum || !pDenom) + return OP_E_NOT_INITIALIZED; + + IMFMediaType* pType = nullptr; + HRESULT hr = pInstance->pSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &pType); + if (SUCCEEDED(hr)) { + hr = MFGetAttributeRatio(pType, MF_MT_FRAME_RATE, pNum, pDenom); + pType->Release(); + } + return hr; +} + +NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG llPositionIn100Ns) { + if (!pInstance || !pInstance->pSourceReader) + return OP_E_NOT_INITIALIZED; + + EnterCriticalSection(&pInstance->csClockSync); + pInstance->bSeekInProgress = TRUE; + LeaveCriticalSection(&pInstance->csClockSync); + + if (pInstance->llPauseStart != 0) { + pInstance->llTotalPauseTime += (GetCurrentTimeMs() - pInstance->llPauseStart); + pInstance->llPauseStart = GetCurrentTimeMs(); + } + + if (pInstance->pLockedBuffer) + UnlockVideoFrame(pInstance); + + // Release cached sample when seeking + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + + // Reset initial frame flag to ensure we read a new frame at the new position + pInstance->bHasInitialFrame = FALSE; + + PROPVARIANT var; + PropVariantInit(&var); + var.vt = VT_I8; + var.hVal.QuadPart = llPositionIn100Ns; + + bool wasPlaying = false; + if (pInstance->bHasAudio && pInstance->pAudioClient) { + wasPlaying = (pInstance->llPauseStart == 0); + pInstance->pAudioClient->Stop(); + Sleep(5); + } + + // Stop the presentation clock + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + pInstance->pPresentationClock->Stop(); + } + + // Seek the main source reader + HRESULT hr = pInstance->pSourceReader->SetCurrentPosition(GUID_NULL, var); + if (FAILED(hr)) { + EnterCriticalSection(&pInstance->csClockSync); + pInstance->bSeekInProgress = FALSE; + LeaveCriticalSection(&pInstance->csClockSync); + PropVariantClear(&var); + return hr; + } + + // Also seek the audio source reader if available + if (pInstance->pSourceReaderAudio) { + PROPVARIANT varAudio; + PropVariantInit(&varAudio); + varAudio.vt = VT_I8; + varAudio.hVal.QuadPart = llPositionIn100Ns; + + HRESULT hrAudio = pInstance->pSourceReaderAudio->SetCurrentPosition(GUID_NULL, varAudio); + if (FAILED(hrAudio)) { + PrintHR("Failed to seek audio source reader", hrAudio); + } + PropVariantClear(&varAudio); + } + + + // Reset audio client if needed + if (pInstance->bHasAudio && pInstance->pRenderClient && pInstance->pAudioClient) { + UINT32 bufferFrameCount = 0; + if (SUCCEEDED(pInstance->pAudioClient->GetBufferSize(&bufferFrameCount))) { + pInstance->pAudioClient->Reset(); + } + } + + PropVariantClear(&var); + + // Update position and state + EnterCriticalSection(&pInstance->csClockSync); + pInstance->llCurrentPosition = llPositionIn100Ns; + pInstance->bSeekInProgress = FALSE; + LeaveCriticalSection(&pInstance->csClockSync); + + pInstance->bEOF = FALSE; + + // IMPORTANT: Reset timing for A/V sync after seek + // We adjust llPlaybackStartTime so that the elapsed time calculation matches the seek position + // Formula: elapsedMs should equal seekPositionMs after seek + // elapsedMs = currentTimeMs - llPlaybackStartTime - llTotalPauseTime + // So: llPlaybackStartTime = currentTimeMs - seekPositionMs / playbackSpeed + if (pInstance->bUseClockSync) { + double seekPositionMs = llPositionIn100Ns / 10000.0; + double adjustedSeekMs = seekPositionMs / static_cast(pInstance->playbackSpeed); + pInstance->llPlaybackStartTime = GetCurrentTimeMs() - static_cast(adjustedSeekMs); + pInstance->llTotalPauseTime = 0; + + // If paused, set pause start to now so pause time accounting works correctly + if (!wasPlaying) { + pInstance->llPauseStart = GetCurrentTimeMs(); + } else { + pInstance->llPauseStart = 0; + } + } + + // Restart the presentation clock at the new position + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + hr = pInstance->pPresentationClock->Start(llPositionIn100Ns); + if (FAILED(hr)) { + PrintHR("Failed to restart presentation clock after seek", hr); + } + } + + // Restart audio if it was playing + if (pInstance->bHasAudio && pInstance->pAudioClient && wasPlaying) { + Sleep(5); + pInstance->pAudioClient->Start(); + } + + // Signal audio thread to continue + if (pInstance->hAudioReadyEvent) + SetEvent(pInstance->hAudioReadyEvent); + + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT GetMediaDuration(const VideoPlayerInstance* pInstance, LONGLONG* pDuration) { + if (!pInstance || !pInstance->pSourceReader || !pDuration) + return OP_E_NOT_INITIALIZED; + + IMFMediaSource* pMediaSource = nullptr; + IMFPresentationDescriptor* pPresentationDescriptor = nullptr; + HRESULT hr = pInstance->pSourceReader->GetServiceForStream(MF_SOURCE_READER_MEDIASOURCE, GUID_NULL, IID_PPV_ARGS(&pMediaSource)); + if (SUCCEEDED(hr)) { + hr = pMediaSource->CreatePresentationDescriptor(&pPresentationDescriptor); + if (SUCCEEDED(hr)) { + hr = pPresentationDescriptor->GetUINT64(MF_PD_DURATION, reinterpret_cast(pDuration)); + pPresentationDescriptor->Release(); + } + pMediaSource->Release(); + } + return hr; +} + +NATIVEVIDEOPLAYER_API HRESULT GetMediaPosition(const VideoPlayerInstance* pInstance, LONGLONG* pPosition) { + if (!pInstance || !pPosition) + return OP_E_NOT_INITIALIZED; + + *pPosition = pInstance->llCurrentPosition; + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, BOOL bPlaying, BOOL bStop) { + if (!pInstance) + return OP_E_NOT_INITIALIZED; + + HRESULT hr = S_OK; + + if (bStop && !bPlaying) { + // Stop playback completely + if (pInstance->llPlaybackStartTime != 0) { + pInstance->llTotalPauseTime = 0; + pInstance->llPauseStart = 0; + pInstance->llPlaybackStartTime = 0; + + // Stop presentation clock + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + pInstance->pPresentationClock->Stop(); + } + + // Stop audio thread if running + if (pInstance->bAudioThreadRunning) { + StopAudioThread(pInstance); + } + + // Reset initial frame flag when stopping + pInstance->bHasInitialFrame = FALSE; + + // Release cached sample when stopping + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + } + } else if (bPlaying) { + // Start or resume playback + if (pInstance->llPlaybackStartTime == 0) { + // First start + pInstance->llPlaybackStartTime = GetCurrentTimeMs(); + } else if (pInstance->llPauseStart != 0) { + // Resume from pause + pInstance->llTotalPauseTime += (GetCurrentTimeMs() - pInstance->llPauseStart); + pInstance->llPauseStart = 0; + } + + // Reset initial frame flag when switching to playing state + pInstance->bHasInitialFrame = FALSE; + + // Start audio client if available + if (pInstance->pAudioClient && pInstance->bAudioInitialized) { + hr = pInstance->pAudioClient->Start(); + if (FAILED(hr)) { + PrintHR("Failed to start audio client", hr); + } + } + + // IMPORTANT: Démarrer le thread audio s'il n'est pas déjà en cours d'exécution + // Ceci est crucial pour le cas où on démarre en pause puis on fait play() + if (pInstance->bHasAudio && pInstance->bAudioInitialized && pInstance->pSourceReaderAudio) { + if (!pInstance->bAudioThreadRunning || pInstance->hAudioThread == nullptr) { + hr = StartAudioThread(pInstance); + if (FAILED(hr)) { + PrintHR("Failed to start audio thread on play", hr); + // Continue anyway - video can still play without audio + } + } + } + + // Start or resume presentation clock + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + // IMPORTANT: Démarrer depuis la position actuelle stockée + hr = pInstance->pPresentationClock->Start(pInstance->llCurrentPosition); + if (FAILED(hr)) { + PrintHR("Failed to start presentation clock", hr); + } + } + + // Signal audio thread to continue if it was waiting + if (pInstance->hAudioReadyEvent) { + SetEvent(pInstance->hAudioReadyEvent); + } + } else { + // Pause playback + if (pInstance->llPauseStart == 0) { + pInstance->llPauseStart = GetCurrentTimeMs(); + } + + // Reset initial frame flag when switching to paused state + pInstance->bHasInitialFrame = FALSE; + + // Pause audio client if available + if (pInstance->pAudioClient && pInstance->bAudioInitialized) { + pInstance->pAudioClient->Stop(); + } + + // Pause presentation clock + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + hr = pInstance->pPresentationClock->Pause(); + if (FAILED(hr)) { + PrintHR("Failed to pause presentation clock", hr); + } + } + + // Note: On ne stoppe PAS le thread audio en pause, on le laisse tourner + // Il va simplement attendre sur les événements de synchronisation + } + return hr; +} + +NATIVEVIDEOPLAYER_API HRESULT ShutdownMediaFoundation() { + return Shutdown(); +} + +NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance) { + if (!pInstance) + return; + + // Stop audio thread + StopAudioThread(pInstance); + + // Release video buffer + if (pInstance->pLockedBuffer) { + UnlockVideoFrame(pInstance); + } + + // Release cached sample + if (pInstance->pCachedSample) { + pInstance->pCachedSample->Release(); + pInstance->pCachedSample = nullptr; + } + + // Reset initial frame flag + pInstance->bHasInitialFrame = FALSE; + + // Macro for safely releasing COM interfaces + #define SAFE_RELEASE(obj) if (obj) { obj->Release(); obj = nullptr; } + + // Stop and release audio resources + if (pInstance->pAudioClient) { + pInstance->pAudioClient->Stop(); + SAFE_RELEASE(pInstance->pAudioClient); + } + + // Stop and release presentation clock + if (pInstance->pPresentationClock) { + pInstance->pPresentationClock->Stop(); + SAFE_RELEASE(pInstance->pPresentationClock); + } + + // Release media source + SAFE_RELEASE(pInstance->pMediaSource); + + // Release other COM resources + SAFE_RELEASE(pInstance->pRenderClient); + SAFE_RELEASE(pInstance->pDevice); + SAFE_RELEASE(pInstance->pAudioEndpointVolume); + SAFE_RELEASE(pInstance->pSourceReader); + SAFE_RELEASE(pInstance->pSourceReaderAudio); + + // Release audio format + if (pInstance->pSourceAudioFormat) { + CoTaskMemFree(pInstance->pSourceAudioFormat); + pInstance->pSourceAudioFormat = nullptr; + } + + // Close event handles + #define SAFE_CLOSE_HANDLE(handle) if (handle) { CloseHandle(handle); handle = nullptr; } + + SAFE_CLOSE_HANDLE(pInstance->hAudioSamplesReadyEvent); + SAFE_CLOSE_HANDLE(pInstance->hAudioReadyEvent); + + // Reset state variables + pInstance->bEOF = FALSE; + pInstance->videoWidth = pInstance->videoHeight = 0; + pInstance->bHasAudio = FALSE; + pInstance->bAudioInitialized = FALSE; + pInstance->llPlaybackStartTime = 0; + pInstance->llTotalPauseTime = 0; + pInstance->llPauseStart = 0; + pInstance->llCurrentPosition = 0; + pInstance->bSeekInProgress = FALSE; + pInstance->playbackSpeed = 1.0f; + + #undef SAFE_RELEASE + #undef SAFE_CLOSE_HANDLE +} + +NATIVEVIDEOPLAYER_API HRESULT SetAudioVolume(VideoPlayerInstance* pInstance, float volume) { + return SetVolume(pInstance, volume); +} + +NATIVEVIDEOPLAYER_API HRESULT GetAudioVolume(const VideoPlayerInstance* pInstance, float* volume) { + return GetVolume(pInstance, volume); +} + +NATIVEVIDEOPLAYER_API HRESULT GetAudioLevels(const VideoPlayerInstance* pInstance, float* pLeftLevel, float* pRightLevel) { + return AudioManager::GetAudioLevels(pInstance, pLeftLevel, pRightLevel); +} + +NATIVEVIDEOPLAYER_API HRESULT SetPlaybackSpeed(VideoPlayerInstance* pInstance, float speed) { + if (!pInstance) + return OP_E_NOT_INITIALIZED; + + // Limit speed between 0.5 and 2.0 + speed = std::max(0.5f, std::min(speed, 2.0f)); + + // Store speed in instance + pInstance->playbackSpeed = speed; + + // Update the presentation clock rate + if (pInstance->bUseClockSync && pInstance->pPresentationClock) { + // Get the rate control interface from the presentation clock + IMFRateControl* pRateControl = nullptr; + HRESULT hr = pInstance->pPresentationClock->QueryInterface(IID_PPV_ARGS(&pRateControl)); + if (SUCCEEDED(hr)) { + // Set the playback rate + hr = pRateControl->SetRate(FALSE, speed); + if (FAILED(hr)) { + PrintHR("Failed to set presentation clock rate", hr); + } + pRateControl->Release(); + } + } + + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInstance, float* pSpeed) { + if (!pInstance || !pSpeed) + return OP_E_INVALID_PARAMETER; + + // Return instance-specific playback speed + *pSpeed = pInstance->playbackSpeed; + + return S_OK; +} + +NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInstance, VideoMetadata* pMetadata) { + if (!pInstance || !pMetadata) + return OP_E_INVALID_PARAMETER; + if (!pInstance->pSourceReader) + return OP_E_NOT_INITIALIZED; + + // Initialize metadata structure with default values + ZeroMemory(pMetadata, sizeof(VideoMetadata)); + + HRESULT hr = S_OK; + + // Get media source for property access + IMFMediaSource* pMediaSource = nullptr; + IMFPresentationDescriptor* pPresentationDescriptor = nullptr; + + // Get media source from source reader + hr = pInstance->pSourceReader->GetServiceForStream( + MF_SOURCE_READER_MEDIASOURCE, + GUID_NULL, + IID_PPV_ARGS(&pMediaSource)); + + if (SUCCEEDED(hr) && pMediaSource) { + // Get presentation descriptor + hr = pMediaSource->CreatePresentationDescriptor(&pPresentationDescriptor); + + if (SUCCEEDED(hr) && pPresentationDescriptor) { + // Get duration + UINT64 duration = 0; + if (SUCCEEDED(pPresentationDescriptor->GetUINT64(MF_PD_DURATION, &duration))) { + pMetadata->duration = static_cast(duration); + pMetadata->hasDuration = TRUE; + } + + // Get stream descriptors to access more metadata + DWORD streamCount = 0; + hr = pPresentationDescriptor->GetStreamDescriptorCount(&streamCount); + + if (SUCCEEDED(hr)) { + // Try to get title and other metadata from attributes + IMFAttributes* pAttributes = nullptr; + if (SUCCEEDED(pPresentationDescriptor->QueryInterface(IID_PPV_ARGS(&pAttributes)))) { + // We can't directly access some metadata attributes due to missing definitions + // Set a default title based on the file path if available + if (pInstance->pSourceReader) { + // For now, we'll leave title empty as we can't reliably extract it + // without the proper attribute definitions + pMetadata->hasTitle = FALSE; + } + + // Try to estimate bitrate from stream properties + UINT64 duration = 0; + if (SUCCEEDED(pPresentationDescriptor->GetUINT64(MF_PD_DURATION, &duration)) && duration > 0) { + // We'll try to estimate bitrate later from individual streams + pMetadata->hasBitrate = FALSE; + } + + pAttributes->Release(); + } + + // Process each stream to get more metadata + for (DWORD i = 0; i < streamCount; i++) { + BOOL selected = FALSE; + IMFStreamDescriptor* pStreamDescriptor = nullptr; + + if (SUCCEEDED(pPresentationDescriptor->GetStreamDescriptorByIndex(i, &selected, &pStreamDescriptor))) { + // Get media type handler + IMFMediaTypeHandler* pHandler = nullptr; + if (SUCCEEDED(pStreamDescriptor->GetMediaTypeHandler(&pHandler))) { + // Get major type to determine if video or audio + GUID majorType; + if (SUCCEEDED(pHandler->GetMajorType(&majorType))) { + if (majorType == MFMediaType_Video) { + // Get current media type + IMFMediaType* pMediaType = nullptr; + if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { + // Get video dimensions + UINT32 width = 0, height = 0; + if (SUCCEEDED(MFGetAttributeSize(pMediaType, MF_MT_FRAME_SIZE, &width, &height))) { + pMetadata->width = width; + pMetadata->height = height; + pMetadata->hasWidth = TRUE; + pMetadata->hasHeight = TRUE; + } + + // Get frame rate + UINT32 numerator = 0, denominator = 1; + if (SUCCEEDED(MFGetAttributeRatio(pMediaType, MF_MT_FRAME_RATE, &numerator, &denominator))) { + if (denominator > 0) { + pMetadata->frameRate = static_cast(numerator) / static_cast(denominator); + pMetadata->hasFrameRate = TRUE; + } + } + + // Get subtype (format) for mime type + GUID subtype; + if (SUCCEEDED(pMediaType->GetGUID(MF_MT_SUBTYPE, &subtype))) { + // Convert subtype to mime type string + if (subtype == MFVideoFormat_H264) { + wcscpy_s(pMetadata->mimeType, L"video/h264"); + pMetadata->hasMimeType = TRUE; + } + else if (subtype == MFVideoFormat_HEVC) { + wcscpy_s(pMetadata->mimeType, L"video/hevc"); + pMetadata->hasMimeType = TRUE; + } + else if (subtype == MFVideoFormat_MPEG2) { + wcscpy_s(pMetadata->mimeType, L"video/mpeg2"); + pMetadata->hasMimeType = TRUE; + } + else if (subtype == MFVideoFormat_WMV3) { + wcscpy_s(pMetadata->mimeType, L"video/wmv"); + pMetadata->hasMimeType = TRUE; + } + else { + wcscpy_s(pMetadata->mimeType, L"video/unknown"); + pMetadata->hasMimeType = TRUE; + } + } + + pMediaType->Release(); + } + } + else if (majorType == MFMediaType_Audio) { + // Get current media type + IMFMediaType* pMediaType = nullptr; + if (SUCCEEDED(pHandler->GetCurrentMediaType(&pMediaType))) { + // Get audio channels + UINT32 channels = 0; + if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels))) { + pMetadata->audioChannels = channels; + pMetadata->hasAudioChannels = TRUE; + } + + // Get audio sample rate + UINT32 sampleRate = 0; + if (SUCCEEDED(pMediaType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &sampleRate))) { + pMetadata->audioSampleRate = sampleRate; + pMetadata->hasAudioSampleRate = TRUE; + } + + pMediaType->Release(); + } + } + } + pHandler->Release(); + } + pStreamDescriptor->Release(); + } + } + } + pPresentationDescriptor->Release(); + } + pMediaSource->Release(); + } + + // If we couldn't get some metadata from the media source, try to get it from the instance + if (!pMetadata->hasWidth || !pMetadata->hasHeight) { + if (pInstance->videoWidth > 0 && pInstance->videoHeight > 0) { + pMetadata->width = pInstance->videoWidth; + pMetadata->height = pInstance->videoHeight; + pMetadata->hasWidth = TRUE; + pMetadata->hasHeight = TRUE; + } + } + + // If we couldn't get frame rate from media source, try to get it directly + if (!pMetadata->hasFrameRate) { + UINT numerator = 0, denominator = 1; + if (SUCCEEDED(GetVideoFrameRate(pInstance, &numerator, &denominator)) && denominator > 0) { + pMetadata->frameRate = static_cast(numerator) / static_cast(denominator); + pMetadata->hasFrameRate = TRUE; + } + } + + // If we couldn't get duration from media source, try to get it directly + if (!pMetadata->hasDuration) { + LONGLONG duration = 0; + if (SUCCEEDED(GetMediaDuration(pInstance, &duration))) { + pMetadata->duration = duration; + pMetadata->hasDuration = TRUE; + } + } + + // If we couldn't get audio channels, check if audio is available + if (!pMetadata->hasAudioChannels && pInstance->bHasAudio) { + if (pInstance->pSourceAudioFormat) { + pMetadata->audioChannels = pInstance->pSourceAudioFormat->nChannels; + pMetadata->hasAudioChannels = TRUE; + + pMetadata->audioSampleRate = pInstance->pSourceAudioFormat->nSamplesPerSec; + pMetadata->hasAudioSampleRate = TRUE; + } + } + + return S_OK; +} diff --git a/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h new file mode 100644 index 00000000..fc060fc7 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/NativeVideoPlayer.h @@ -0,0 +1,239 @@ +// NativeVideoPlayer.h +#pragma once +#ifndef NATIVE_VIDEO_PLAYER_H +#define NATIVE_VIDEO_PLAYER_H + +#include +#include +#include +#include +#include +#include + +// Structure to hold video metadata +typedef struct VideoMetadata { + wchar_t title[256]; // Title of the video (empty if not available) + LONGLONG duration; // Duration in 100-ns units + UINT32 width; // Width in pixels + UINT32 height; // Height in pixels + LONGLONG bitrate; // Bitrate in bits per second + float frameRate; // Frame rate in frames per second + wchar_t mimeType[64]; // MIME type of the video + UINT32 audioChannels; // Number of audio channels + UINT32 audioSampleRate; // Audio sample rate in Hz + BOOL hasTitle; // TRUE if title is available + BOOL hasDuration; // TRUE if duration is available + BOOL hasWidth; // TRUE if width is available + BOOL hasHeight; // TRUE if height is available + BOOL hasBitrate; // TRUE if bitrate is available + BOOL hasFrameRate; // TRUE if frame rate is available + BOOL hasMimeType; // TRUE if MIME type is available + BOOL hasAudioChannels; // TRUE if audio channels is available + BOOL hasAudioSampleRate; // TRUE if audio sample rate is available +} VideoMetadata; + +// Macro d'exportation pour la DLL Windows +#ifdef _WIN32 +#ifdef NATIVEVIDEOPLAYER_EXPORTS +#define NATIVEVIDEOPLAYER_API __declspec(dllexport) +#else +#define NATIVEVIDEOPLAYER_API __declspec(dllimport) +#endif +#else +#define NATIVEVIDEOPLAYER_API +#endif + +// Codes d'erreur personnalisés +#define OP_E_NOT_INITIALIZED ((HRESULT)0x80000001L) +#define OP_E_ALREADY_INITIALIZED ((HRESULT)0x80000002L) +#define OP_E_INVALID_PARAMETER ((HRESULT)0x80000003L) + +// Structure pour encapsuler l'état d'une instance de lecteur vidéo +struct VideoPlayerInstance; + +#ifdef __cplusplus +extern "C" { +#endif + +// ==================================================================== +// Fonctions exportées pour la gestion des instances et la lecture multimédia +// ==================================================================== + +/** + * @brief Initialise Media Foundation, Direct3D11 et le gestionnaire DXGI (une seule fois pour toutes les instances). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT InitMediaFoundation(); + +/** + * @brief Crée une nouvelle instance de lecteur vidéo. + * @param ppInstance Pointeur pour recevoir le handle de l'instance créée. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT CreateVideoPlayerInstance(VideoPlayerInstance** ppInstance); + +/** + * @brief Détruit une instance de lecteur vidéo et libère ses ressources. + * @param pInstance Handle de l'instance à détruire. + */ +NATIVEVIDEOPLAYER_API void DestroyVideoPlayerInstance(VideoPlayerInstance* pInstance); + +/** + * @brief Ouvre un média (fichier ou URL) et prépare le décodage avec accélération matérielle pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param url Chemin ou URL du média (chaîne large). + * @param startPlayback TRUE pour démarrer la lecture immédiatement, FALSE pour rester en pause. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT OpenMedia(VideoPlayerInstance* pInstance, const wchar_t* url, BOOL startPlayback = TRUE); + +/** + * @brief Lit la prochaine frame vidéo en format RGB32 pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pData Reçoit un pointeur sur les données de la frame (à ne pas libérer). + * @param pDataSize Reçoit la taille en octets du tampon. + * @return S_OK si une frame est lue, S_FALSE en fin de flux, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrame(VideoPlayerInstance* pInstance, BYTE** pData, DWORD* pDataSize); + +/** + * @brief Déverrouille le tampon de la frame vidéo précédemment verrouillé pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @return S_OK en cas de succès. + */ +NATIVEVIDEOPLAYER_API HRESULT UnlockVideoFrame(VideoPlayerInstance* pInstance); + +/* + * Reads the next video frame and copies it into a destination buffer. + * pTimestamp receives the 100ns timestamp when available. + */ +NATIVEVIDEOPLAYER_API HRESULT ReadVideoFrameInto( + VideoPlayerInstance* pInstance, + BYTE* pDst, + DWORD dstRowBytes, + DWORD dstCapacity, + LONGLONG* pTimestamp); + +/** + * @brief Ferme le média et libère les ressources associées pour une instance spécifique. + * @param pInstance Handle de l'instance. + */ +NATIVEVIDEOPLAYER_API void CloseMedia(VideoPlayerInstance* pInstance); + +/** + * @brief Indique si la fin du flux média a été atteinte pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @return TRUE si fin de flux, FALSE sinon. + */ +NATIVEVIDEOPLAYER_API BOOL IsEOF(const VideoPlayerInstance* pInstance); + +/** + * @brief Récupère les dimensions de la vidéo pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pWidth Pointeur pour recevoir la largeur en pixels. + * @param pHeight Pointeur pour recevoir la hauteur en pixels. + */ +NATIVEVIDEOPLAYER_API void GetVideoSize(const VideoPlayerInstance* pInstance, UINT32* pWidth, UINT32* pHeight); + +/** + * @brief Récupère le taux de rafraîchissement (frame rate) de la vidéo pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pNum Pointeur pour recevoir le numérateur. + * @param pDenom Pointeur pour recevoir le dénominateur. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetVideoFrameRate(const VideoPlayerInstance* pInstance, UINT* pNum, UINT* pDenom); + +/** + * @brief Recherche une position spécifique dans le média pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param llPosition Position (en 100-ns) à atteindre. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT SeekMedia(VideoPlayerInstance* pInstance, LONGLONG llPosition); + +/** + * @brief Obtient la durée totale du média pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pDuration Pointeur pour recevoir la durée (en 100-ns). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetMediaDuration(const VideoPlayerInstance* pInstance, LONGLONG* pDuration); + +/** + * @brief Obtient la position de lecture courante pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pPosition Pointeur pour recevoir la position (en 100-ns). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetMediaPosition(const VideoPlayerInstance* pInstance, LONGLONG* pPosition); + +/** + * @brief Définit l'état de lecture (lecture ou pause) pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param bPlaying TRUE pour lecture, FALSE pour pause. + * @param bStop TRUE si c'est un arrêt complet, FALSE si c'est simplement une pause. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT SetPlaybackState(VideoPlayerInstance* pInstance, BOOL bPlaying, BOOL bStop = FALSE); + +/** + * @brief Arrête Media Foundation et libère les ressources globales (après destruction de toutes les instances). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT ShutdownMediaFoundation(); + +/** + * @brief Définit le niveau de volume audio pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param volume Niveau de volume (0.0 à 1.0). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT SetAudioVolume(VideoPlayerInstance* pInstance, float volume); + +/** + * @brief Récupère le niveau de volume audio actuel pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param volume Pointeur pour recevoir le niveau de volume (0.0 à 1.0). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetAudioVolume(const VideoPlayerInstance* pInstance, float* volume); + +/** + * @brief Récupère les niveaux audio pour les canaux gauche et droit pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pLeftLevel Pointeur pour le niveau du canal gauche. + * @param pRightLevel Pointeur pour le niveau du canal droit. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetAudioLevels(const VideoPlayerInstance* pInstance, float* pLeftLevel, float* pRightLevel); + +/** + * @brief Définit la vitesse de lecture pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param speed Vitesse de lecture (0.5 à 2.0, où 1.0 est la vitesse normale). + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT SetPlaybackSpeed(VideoPlayerInstance* pInstance, float speed); + +/** + * @brief Récupère la vitesse de lecture actuelle pour une instance spécifique. + * @param pInstance Handle de l'instance. + * @param pSpeed Pointeur pour recevoir la vitesse de lecture. + * @return S_OK en cas de succès, ou un code d'erreur. + */ +NATIVEVIDEOPLAYER_API HRESULT GetPlaybackSpeed(const VideoPlayerInstance* pInstance, float* pSpeed); + +/** + * @brief Retrieves all available metadata for the current media. + * @param pInstance Handle to the instance. + * @param pMetadata Pointer to receive the metadata structure. + * @return S_OK on success, or an error code. + */ +NATIVEVIDEOPLAYER_API HRESULT GetVideoMetadata(const VideoPlayerInstance* pInstance, VideoMetadata* pMetadata); + +#ifdef __cplusplus +} +#endif + +#endif // NATIVE_VIDEO_PLAYER_H diff --git a/mediaplayer/src/jvmMain/native/windows/Utils.cpp b/mediaplayer/src/jvmMain/native/windows/Utils.cpp new file mode 100644 index 00000000..78724730 --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/Utils.cpp @@ -0,0 +1,24 @@ +#include "Utils.h" +#include +#include + +namespace VideoPlayerUtils { + +void PreciseSleepHighRes(double ms) { + if (ms <= 0.1) + return; + + // Use a single static timer for all sleep operations + static HANDLE hTimer = CreateWaitableTimer(nullptr, TRUE, nullptr); + if (!hTimer) { + std::this_thread::sleep_for(std::chrono::duration(ms)); + return; + } + + LARGE_INTEGER liDueTime; + liDueTime.QuadPart = -static_cast(ms * 10000.0); + SetWaitableTimer(hTimer, &liDueTime, 0, nullptr, nullptr, FALSE); + WaitForSingleObject(hTimer, INFINITE); +} + +} // namespace VideoPlayerUtils \ No newline at end of file diff --git a/mediaplayer/src/jvmMain/native/windows/Utils.h b/mediaplayer/src/jvmMain/native/windows/Utils.h new file mode 100644 index 00000000..81f2aaca --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/Utils.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace VideoPlayerUtils { + +/** + * @brief Gets the current time in milliseconds. + * @return Current time in milliseconds. + */ +inline ULONGLONG GetCurrentTimeMs() { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + +/** + * @brief Performs a high-resolution sleep for the specified duration. + * @param ms Sleep duration in milliseconds. + */ +void PreciseSleepHighRes(double ms); + +} // namespace VideoPlayerUtils \ No newline at end of file diff --git a/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h new file mode 100644 index 00000000..02581efa --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/VideoPlayerInstance.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Structure to encapsulate the state of a video player instance. + */ +struct VideoPlayerInstance { + // Video related members + IMFSourceReader* pSourceReader = nullptr; + IMFMediaBuffer* pLockedBuffer = nullptr; + BYTE* pLockedBytes = nullptr; + DWORD lockedMaxSize = 0; + DWORD lockedCurrSize = 0; + UINT32 videoWidth = 0; + UINT32 videoHeight = 0; + BOOL bEOF = FALSE; + + // Frame caching for paused state + IMFSample* pCachedSample = nullptr; // Cached sample for paused state + BOOL bHasInitialFrame = FALSE; // Whether we've read an initial frame when paused + + // Audio related members + IMFSourceReader* pSourceReaderAudio = nullptr; + BOOL bHasAudio = FALSE; + BOOL bAudioInitialized = FALSE; + IAudioClient* pAudioClient = nullptr; + IAudioRenderClient* pRenderClient = nullptr; + IMMDevice* pDevice = nullptr; + WAVEFORMATEX* pSourceAudioFormat = nullptr; + HANDLE hAudioSamplesReadyEvent = nullptr; + HANDLE hAudioThread = nullptr; + BOOL bAudioThreadRunning = FALSE; + HANDLE hAudioReadyEvent = nullptr; + IAudioEndpointVolume* pAudioEndpointVolume = nullptr; + + // Media Foundation clock for synchronization + IMFPresentationClock* pPresentationClock = nullptr; + IMFMediaSource* pMediaSource = nullptr; + BOOL bUseClockSync = FALSE; + + // Timing and synchronization + LONGLONG llCurrentPosition = 0; + ULONGLONG llPlaybackStartTime = 0; + ULONGLONG llTotalPauseTime = 0; + ULONGLONG llPauseStart = 0; + CRITICAL_SECTION csClockSync{}; + BOOL bSeekInProgress = FALSE; + + // Playback control + float instanceVolume = 1.0f; // Volume specific to this instance (1.0 = 100%) + float playbackSpeed = 1.0f; // Playback speed (1.0 = 100%) +}; diff --git a/mediaplayer/src/jvmMain/native/windows/build.bat b/mediaplayer/src/jvmMain/native/windows/build.bat new file mode 100644 index 00000000..18f0ab9b --- /dev/null +++ b/mediaplayer/src/jvmMain/native/windows/build.bat @@ -0,0 +1,49 @@ +@echo off +setlocal + +echo === Starting compilation for x64 and ARM64 === + +echo. +echo === x64 Configuration === +cmake -B build-x64 -A x64 . +if %ERRORLEVEL% neq 0 ( + echo Error during x64 configuration + exit /b %ERRORLEVEL% +) + +echo. +echo === x64 Compilation === +cmake --build build-x64 --config Release +if %ERRORLEVEL% neq 0 ( + echo Error during x64 compilation + exit /b %ERRORLEVEL% +) + +echo. +echo === ARM64 Configuration === +cmake -B build-arm64 -A ARM64 . +if %ERRORLEVEL% neq 0 ( + echo Error during ARM64 configuration + exit /b %ERRORLEVEL% +) + +echo. +echo === ARM64 Compilation === +cmake --build build-arm64 --config Release +if %ERRORLEVEL% neq 0 ( + echo Error during ARM64 compilation + exit /b %ERRORLEVEL% +) + +echo. +echo === Compilation completed successfully for both architectures === +echo. + +rem Clean up build directories +if exist build-x64 rmdir /s /q build-x64 +if exist build-arm64 rmdir /s /q build-arm64 + +echo x64 DLL: ..\..\resources\win32-x86-64\NativeVideoPlayer.dll +echo ARM64 DLL: ..\..\resources\win32-arm64\NativeVideoPlayer.dll + +endlocal diff --git a/mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib b/mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib deleted file mode 100755 index b1bfe7f003ac15a097fc2a71392471ee224cf651..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201560 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 diff --git a/mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib b/mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib deleted file mode 100755 index c553e75e11cb2e35579b18399f3eb35213180353..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179624 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%0DYRWFOp<3YGIuQ4`D&*BB6**e))&=q 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 diff --git a/mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll b/mediaplayer/src/jvmMain/resources/win32-x86-64/NativeVideoPlayer.dll deleted file mode 100644 index 20e025d6204886bacd21e8f31a18a769d8fb7a99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31744 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 diff --git a/winlib b/winlib deleted file mode 160000 index a505e660..00000000 --- a/winlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a505e660530bd8cbc721769343014cf79fb661d9 From 3052d2fd8b563c9ff9d58ddd1c872f008d26862c Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:45:20 +0200 Subject: [PATCH 2/7] Fix deprecation warnings in build scripts - Remove SonatypeHost.CENTRAL_PORTAL parameter (OSSRH sunset) - Suppress androidTarget deprecation (future AGP 9.0 change) --- mediaplayer/build.gradle.kts | 5 ++--- sample/composeApp/build.gradle.kts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index d73e40a4..8093b759 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -1,6 +1,5 @@ @file:OptIn(ExperimentalWasmDsl::class) -import com.vanniktech.maven.publish.SonatypeHost import org.apache.tools.ant.taskdefs.condition.Os import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl @@ -33,6 +32,7 @@ tasks.withType().configureEach { kotlin { jvmToolchain(17) + @Suppress("DEPRECATION") androidTarget { publishLibraryVariants("release") } jvm() js { @@ -242,7 +242,6 @@ mavenPublishing { } } - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - + publishToMavenCentral() signAllPublications() } diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index 08c92fce..a8e84c10 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -15,6 +15,7 @@ plugins { kotlin { jvmToolchain(17) + @Suppress("DEPRECATION") androidTarget() jvm() js(IR) { From d2f31e7c8d8108fcf80d01a287806b53129af9d7 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:46:36 +0200 Subject: [PATCH 3/7] Fix build errors: use jvmProcessResources for KMP project - Replace tasks.processResources with configureEach matching jvmProcessResources - Add OptIn for transitiveExport experimental API --- mediaplayer/build.gradle.kts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index 8093b759..f863ebcc 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -67,7 +67,8 @@ kotlin { framework { baseName = "ComposeMediaPlayer" isStatic = false - transitiveExport = false // This is default. + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) + transitiveExport = false } // Maps custom Xcode configuration to NativeBuildType @@ -196,12 +197,8 @@ val buildNativeWindows by tasks.registering(Exec::class) { commandLine("cmd", "/c", nativeDir.file("build.bat").asFile.absolutePath) } -tasks.processResources { - dependsOn(buildNativeMacOs, buildNativeWindows) -} - tasks.configureEach { - if (name == "sourcesJar") { + if (name == "jvmProcessResources" || name == "sourcesJar") { dependsOn(buildNativeMacOs, buildNativeWindows) } } From 4a85efe7c1a61efac2d483c3b1c64c4538334630 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:47:05 +0200 Subject: [PATCH 4/7] Add back buildNativeLibraries aggregation task --- mediaplayer/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index f863ebcc..2a35dc26 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -197,6 +197,12 @@ val buildNativeWindows by tasks.registering(Exec::class) { commandLine("cmd", "/c", nativeDir.file("build.bat").asFile.absolutePath) } +tasks.register("buildNativeLibraries") { + group = "build" + description = "Builds all native libraries (macOS + Windows)" + dependsOn(buildNativeMacOs, buildNativeWindows) +} + tasks.configureEach { if (name == "jvmProcessResources" || name == "sourcesJar") { dependsOn(buildNativeMacOs, buildNativeWindows) From 1620c5d0ce064edfd0db5f0320e24c9d59b37de1 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:48:29 +0200 Subject: [PATCH 5/7] Align native build tasks with ComposeDeskKit pattern --- mediaplayer/build.gradle.kts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index 2a35dc26..502a319e 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -197,14 +197,12 @@ val buildNativeWindows by tasks.registering(Exec::class) { commandLine("cmd", "/c", nativeDir.file("build.bat").asFile.absolutePath) } -tasks.register("buildNativeLibraries") { - group = "build" - description = "Builds all native libraries (macOS + Windows)" +tasks.named("jvmProcessResources") { dependsOn(buildNativeMacOs, buildNativeWindows) } tasks.configureEach { - if (name == "jvmProcessResources" || name == "sourcesJar") { + if (name == "sourcesJar") { dependsOn(buildNativeMacOs, buildNativeWindows) } } From 59db18229254e2c961d5e23bd9ff5b059f18ebfe Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:49:42 +0200 Subject: [PATCH 6/7] Update run configurations for new native paths --- .run/BuildNativeAndRunDemo.run.xml | 39 ++++++++++++++++++------------ .run/CompileSwiftLib.run.xml | 4 +-- .run/CompileWinLib.run.xml | 6 ++--- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.run/BuildNativeAndRunDemo.run.xml b/.run/BuildNativeAndRunDemo.run.xml index d83fcabf..a7b92bfd 100644 --- a/.run/BuildNativeAndRunDemo.run.xml +++ b/.run/BuildNativeAndRunDemo.run.xml @@ -1,20 +1,27 @@ - - \ No newline at end of file + diff --git a/.run/CompileSwiftLib.run.xml b/.run/CompileSwiftLib.run.xml index be4c4c5f..cb22f970 100644 --- a/.run/CompileSwiftLib.run.xml +++ b/.run/CompileSwiftLib.run.xml @@ -1,6 +1,6 @@ - - \ No newline at end of file + diff --git a/.run/CompileWinLib.run.xml b/.run/CompileWinLib.run.xml index a225c499..0ef5e236 100644 --- a/.run/CompileWinLib.run.xml +++ b/.run/CompileWinLib.run.xml @@ -2,10 +2,10 @@ - \ No newline at end of file + From 5f088d79e9a2b9c4f4ff455e2698cc8abdb33bab Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Sun, 8 Mar 2026 00:52:25 +0200 Subject: [PATCH 7/7] Fix Windows CI: set working-directory for build.bat --- .github/workflows/build-natives.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-natives.yml b/.github/workflows/build-natives.yml index 8bf6d6f8..d8bbcd37 100644 --- a/.github/workflows/build-natives.yml +++ b/.github/workflows/build-natives.yml @@ -12,7 +12,8 @@ jobs: - name: Build Windows native DLLs shell: cmd - run: call mediaplayer\src\jvmMain\native\windows\build.bat + working-directory: mediaplayer/src/jvmMain/native/windows + run: call build.bat - name: Verify Windows natives shell: bash