|
1 | 1 | package io.github.kdroidfilter.composemediaplayer.windows |
2 | 2 |
|
3 | | -import com.sun.jna.Native |
4 | | -import com.sun.jna.Pointer |
5 | | -import com.sun.jna.Structure |
6 | | -import com.sun.jna.WString |
7 | | -import com.sun.jna.ptr.FloatByReference |
8 | | -import com.sun.jna.ptr.IntByReference |
9 | | -import com.sun.jna.ptr.LongByReference |
10 | | -import com.sun.jna.ptr.PointerByReference |
11 | 3 | import io.github.kdroidfilter.composemediaplayer.VideoMetadata |
| 4 | +import java.io.File |
| 5 | +import java.nio.ByteBuffer |
| 6 | +import java.nio.file.Files |
12 | 7 |
|
13 | 8 | internal object MediaFoundationLib { |
14 | | - /** |
15 | | - * Register the native library for JNA direct mapping |
16 | | - */ |
17 | | - init { |
18 | | - Native.register("NativeVideoPlayer") |
19 | | - } |
| 9 | + /** Expected native API version — must match NATIVE_VIDEO_PLAYER_VERSION in the DLL. */ |
| 10 | + private const val EXPECTED_NATIVE_VERSION = 2 |
20 | 11 |
|
21 | | - /** |
22 | | - * JNA structure that maps to the C++ VideoMetadata structure |
23 | | - */ |
24 | | - @Structure.FieldOrder( |
25 | | - "title", "duration", "width", "height", "bitrate", "frameRate", "mimeType", |
26 | | - "audioChannels", "audioSampleRate", "hasTitle", "hasDuration", "hasWidth", |
27 | | - "hasHeight", "hasBitrate", "hasFrameRate", "hasMimeType", "hasAudioChannels", |
28 | | - "hasAudioSampleRate" |
29 | | - ) |
30 | | - class NativeVideoMetadata : Structure() { |
31 | | - @JvmField var title = CharArray(256) |
32 | | - @JvmField var duration: Long = 0 |
33 | | - @JvmField var width: Int = 0 |
34 | | - @JvmField var height: Int = 0 |
35 | | - @JvmField var bitrate: Long = 0 |
36 | | - @JvmField var frameRate: Float = 0f |
37 | | - @JvmField var mimeType = CharArray(64) |
38 | | - @JvmField var audioChannels: Int = 0 |
39 | | - @JvmField var audioSampleRate: Int = 0 |
40 | | - @JvmField var hasTitle: Boolean = false |
41 | | - @JvmField var hasDuration: Boolean = false |
42 | | - @JvmField var hasWidth: Boolean = false |
43 | | - @JvmField var hasHeight: Boolean = false |
44 | | - @JvmField var hasBitrate: Boolean = false |
45 | | - @JvmField var hasFrameRate: Boolean = false |
46 | | - @JvmField var hasMimeType: Boolean = false |
47 | | - @JvmField var hasAudioChannels: Boolean = false |
48 | | - @JvmField var hasAudioSampleRate: Boolean = false |
49 | | - |
50 | | - /** |
51 | | - * Converts this native structure to a Kotlin VideoMetadata object |
52 | | - */ |
53 | | - fun toVideoMetadata(): VideoMetadata { |
54 | | - return VideoMetadata( |
55 | | - title = if (hasTitle) String(title).trim { it <= ' ' || it == '\u0000' } else null, |
56 | | - duration = if (hasDuration) duration / 10000 else null, // Convert from 100ns to ms |
57 | | - width = if (hasWidth) width else null, |
58 | | - height = if (hasHeight) height else null, |
59 | | - bitrate = if (hasBitrate) bitrate else null, |
60 | | - frameRate = if (hasFrameRate) frameRate else null, |
61 | | - mimeType = if (hasMimeType) String(mimeType).trim { it <= ' ' || it == '\u0000' } else null, |
62 | | - audioChannels = if (hasAudioChannels) audioChannels else null, |
63 | | - audioSampleRate = if (hasAudioSampleRate) audioSampleRate else null |
64 | | - ) |
| 12 | + init { |
| 13 | + loadNativeLibrary() |
| 14 | + val nativeVersion = nGetNativeVersion() |
| 15 | + require(nativeVersion == EXPECTED_NATIVE_VERSION) { |
| 16 | + "NativeVideoPlayer DLL version mismatch: expected $EXPECTED_NATIVE_VERSION but got $nativeVersion. " + |
| 17 | + "Please rebuild the native DLL or update the Kotlin bindings." |
65 | 18 | } |
66 | 19 | } |
67 | 20 |
|
68 | | - /** |
69 | | - * Helper: Creates a new instance of the native video player |
70 | | - * @return A pointer to the native instance or null if creation failed |
71 | | - */ |
72 | | - fun createInstance(): Pointer? { |
73 | | - val ptrRef = PointerByReference() |
74 | | - val hr = CreateVideoPlayerInstance(ptrRef) |
75 | | - return if (hr >= 0 && ptrRef.value != null) ptrRef.value else null |
| 21 | + private fun loadNativeLibrary() { |
| 22 | + val osArch = System.getProperty("os.arch", "").lowercase() |
| 23 | + val resourceDir = |
| 24 | + if (osArch == "aarch64" || osArch == "arm64") "win32-arm64" else "win32-x86-64" |
| 25 | + val libName = "NativeVideoPlayer.dll" |
| 26 | + |
| 27 | + val stream = MediaFoundationLib::class.java.getResourceAsStream("/$resourceDir/$libName") |
| 28 | + ?: throw UnsatisfiedLinkError("Native library not found in resources: /$resourceDir/$libName") |
| 29 | + |
| 30 | + val tempDir = Files.createTempDirectory("nativevideoplayer").toFile() |
| 31 | + val tempFile = File(tempDir, libName) |
| 32 | + stream.use { input -> tempFile.outputStream().use { input.copyTo(it) } } |
| 33 | + System.load(tempFile.absolutePath) |
| 34 | + tempFile.deleteOnExit() |
| 35 | + tempDir.deleteOnExit() |
76 | 36 | } |
77 | 37 |
|
78 | | - /** |
79 | | - * Helper: Destroys a native video player instance |
80 | | - * @param instance The pointer to the native instance to destroy |
81 | | - */ |
82 | | - fun destroyInstance(instance: Pointer) { |
83 | | - DestroyVideoPlayerInstance(instance) |
| 38 | + // ----- Helpers ----- |
| 39 | + |
| 40 | + fun createInstance(): Long { |
| 41 | + val handle = nCreateInstance() |
| 42 | + return if (handle != 0L) handle else 0L |
84 | 43 | } |
85 | 44 |
|
86 | | - /** |
87 | | - * Helper: Retrieves metadata for the current media |
88 | | - * @param instance Pointer to the native instance |
89 | | - * @return VideoMetadata object containing all available metadata, or null if retrieval failed |
90 | | - */ |
91 | | - fun getVideoMetadata(instance: Pointer): VideoMetadata? { |
92 | | - val metadata = NativeVideoMetadata() |
93 | | - val hr = GetVideoMetadata(instance, metadata) |
94 | | - return if (hr >= 0) metadata.toVideoMetadata() else null |
| 45 | + fun destroyInstance(handle: Long) = nDestroyInstance(handle) |
| 46 | + |
| 47 | + fun getVideoMetadata(handle: Long): VideoMetadata? { |
| 48 | + val title = CharArray(256) |
| 49 | + val mimeType = CharArray(64) |
| 50 | + val longVals = LongArray(2) |
| 51 | + val intVals = IntArray(4) |
| 52 | + val floatVals = FloatArray(1) |
| 53 | + val hasFlags = BooleanArray(9) |
| 54 | + |
| 55 | + val hr = nGetVideoMetadata(handle, title, mimeType, longVals, intVals, floatVals, hasFlags) |
| 56 | + if (hr < 0) return null |
| 57 | + |
| 58 | + return VideoMetadata( |
| 59 | + title = if (hasFlags[0]) String(title).trim { it <= ' ' || it == '\u0000' } else null, |
| 60 | + duration = if (hasFlags[1]) longVals[0] / 10000 else null, |
| 61 | + width = if (hasFlags[2]) intVals[0] else null, |
| 62 | + height = if (hasFlags[3]) intVals[1] else null, |
| 63 | + bitrate = if (hasFlags[4]) longVals[1] else null, |
| 64 | + frameRate = if (hasFlags[5]) floatVals[0] else null, |
| 65 | + mimeType = if (hasFlags[6]) String(mimeType).trim { it <= ' ' || it == '\u0000' } else null, |
| 66 | + audioChannels = if (hasFlags[7]) intVals[2] else null, |
| 67 | + audioSampleRate = if (hasFlags[8]) intVals[3] else null, |
| 68 | + ) |
95 | 69 | } |
96 | 70 |
|
97 | | - // === Direct mapped native methods === |
98 | | - @JvmStatic external fun InitMediaFoundation(): Int |
99 | | - @JvmStatic external fun CreateVideoPlayerInstance(ppInstance: PointerByReference): Int |
100 | | - @JvmStatic external fun DestroyVideoPlayerInstance(pInstance: Pointer) |
101 | | - @JvmStatic external fun OpenMedia(pInstance: Pointer, url: WString, startPlayback: Boolean): Int |
102 | | - @JvmStatic external fun ReadVideoFrame(pInstance: Pointer, pData: PointerByReference, pDataSize: IntByReference): Int |
103 | | - @JvmStatic external fun UnlockVideoFrame(pInstance: Pointer): Int |
104 | | - @JvmStatic external fun CloseMedia(pInstance: Pointer) |
105 | | - @JvmStatic external fun IsEOF(pInstance: Pointer): Boolean |
106 | | - @JvmStatic external fun GetVideoSize(pInstance: Pointer, pWidth: IntByReference, pHeight: IntByReference) |
107 | | - @JvmStatic external fun GetVideoFrameRate(pInstance: Pointer, pNum: IntByReference, pDenom: IntByReference): Int |
108 | | - @JvmStatic external fun SeekMedia(pInstance: Pointer, lPosition: Long): Int |
109 | | - @JvmStatic external fun GetMediaDuration(pInstance: Pointer, pDuration: LongByReference): Int |
110 | | - @JvmStatic external fun GetMediaPosition(pInstance: Pointer, pPosition: LongByReference): Int |
111 | | - @JvmStatic external fun SetPlaybackState(pInstance: Pointer, isPlaying: Boolean, bStop: Boolean): Int |
112 | | - @JvmStatic external fun ShutdownMediaFoundation(): Int |
113 | | - @JvmStatic external fun SetAudioVolume(pInstance: Pointer, volume: Float): Int |
114 | | - @JvmStatic external fun GetAudioVolume(pInstance: Pointer, volume: FloatByReference): Int |
115 | | - @JvmStatic external fun GetAudioLevels(pInstance: Pointer, pLeftLevel: FloatByReference, pRightLevel: FloatByReference): Int |
116 | | - @JvmStatic external fun SetPlaybackSpeed(pInstance: Pointer, speed: Float): Int |
117 | | - @JvmStatic external fun GetPlaybackSpeed(pInstance: Pointer, pSpeed: FloatByReference): Int |
118 | | - |
119 | | - /** |
120 | | - * Retrieves all available metadata for the current media |
121 | | - * @param pInstance Pointer to the native instance |
122 | | - * @param pMetadata Pointer to receive the metadata structure |
123 | | - * @return S_OK on success, or an error code |
124 | | - */ |
125 | | - @JvmStatic external fun GetVideoMetadata(pInstance: Pointer, pMetadata: NativeVideoMetadata): Int |
| 71 | + // ----- JNI native methods (registered via JNI_OnLoad / RegisterNatives) ----- |
| 72 | + |
| 73 | + @JvmStatic external fun nGetNativeVersion(): Int |
| 74 | + @JvmStatic external fun nInitMediaFoundation(): Int |
| 75 | + @JvmStatic external fun nCreateInstance(): Long |
| 76 | + @JvmStatic external fun nDestroyInstance(handle: Long) |
| 77 | + @JvmStatic external fun nOpenMedia(handle: Long, url: String, startPlayback: Boolean): Int |
| 78 | + @JvmStatic external fun nReadVideoFrame(handle: Long, outResult: IntArray): ByteBuffer? |
| 79 | + @JvmStatic external fun nUnlockVideoFrame(handle: Long): Int |
| 80 | + @JvmStatic external fun nCloseMedia(handle: Long) |
| 81 | + @JvmStatic external fun nIsEOF(handle: Long): Boolean |
| 82 | + @JvmStatic external fun nGetVideoSize(handle: Long, outSize: IntArray) |
| 83 | + @JvmStatic external fun nGetVideoFrameRate(handle: Long, outRate: IntArray): Int |
| 84 | + @JvmStatic external fun nSeekMedia(handle: Long, position: Long): Int |
| 85 | + @JvmStatic external fun nGetMediaDuration(handle: Long, outDuration: LongArray): Int |
| 86 | + @JvmStatic external fun nGetMediaPosition(handle: Long, outPosition: LongArray): Int |
| 87 | + @JvmStatic external fun nSetPlaybackState(handle: Long, isPlaying: Boolean, stop: Boolean): Int |
| 88 | + @JvmStatic external fun nShutdownMediaFoundation(): Int |
| 89 | + @JvmStatic external fun nSetAudioVolume(handle: Long, volume: Float): Int |
| 90 | + @JvmStatic external fun nGetAudioVolume(handle: Long, outVolume: FloatArray): Int |
| 91 | + @JvmStatic external fun nGetAudioLevels(handle: Long, outLevels: FloatArray): Int |
| 92 | + @JvmStatic external fun nSetPlaybackSpeed(handle: Long, speed: Float): Int |
| 93 | + @JvmStatic external fun nGetPlaybackSpeed(handle: Long, outSpeed: FloatArray): Int |
| 94 | + @JvmStatic external fun nWrapPointer(address: Long, size: Long): ByteBuffer? |
| 95 | + @JvmStatic external fun nSetOutputSize(handle: Long, width: Int, height: Int): Int |
| 96 | + |
| 97 | + @JvmStatic private external fun nGetVideoMetadata( |
| 98 | + handle: Long, title: CharArray, mimeType: CharArray, |
| 99 | + longVals: LongArray, intVals: IntArray, floatVals: FloatArray, hasFlags: BooleanArray |
| 100 | + ): Int |
| 101 | + |
| 102 | + // ----- Convenience wrappers (keep old API names for minimal caller changes) ----- |
| 103 | + |
| 104 | + fun InitMediaFoundation(): Int = nInitMediaFoundation() |
| 105 | + fun ShutdownMediaFoundation(): Int = nShutdownMediaFoundation() |
| 106 | + fun OpenMedia(handle: Long, url: String, startPlayback: Boolean): Int = nOpenMedia(handle, url, startPlayback) |
| 107 | + fun CloseMedia(handle: Long) = nCloseMedia(handle) |
| 108 | + fun IsEOF(handle: Long): Boolean = nIsEOF(handle) |
| 109 | + fun UnlockVideoFrame(handle: Long): Int = nUnlockVideoFrame(handle) |
| 110 | + fun SeekMedia(handle: Long, position: Long): Int = nSeekMedia(handle, position) |
| 111 | + fun SetPlaybackState(handle: Long, isPlaying: Boolean, stop: Boolean): Int = nSetPlaybackState(handle, isPlaying, stop) |
| 112 | + fun SetAudioVolume(handle: Long, volume: Float): Int = nSetAudioVolume(handle, volume) |
| 113 | + fun SetPlaybackSpeed(handle: Long, speed: Float): Int = nSetPlaybackSpeed(handle, speed) |
| 114 | + |
| 115 | + fun ReadVideoFrame(handle: Long, outResult: IntArray): ByteBuffer? = nReadVideoFrame(handle, outResult) |
| 116 | + fun GetVideoSize(handle: Long, outSize: IntArray) = nGetVideoSize(handle, outSize) |
| 117 | + fun GetMediaDuration(handle: Long, outDuration: LongArray): Int = nGetMediaDuration(handle, outDuration) |
| 118 | + fun GetMediaPosition(handle: Long, outPosition: LongArray): Int = nGetMediaPosition(handle, outPosition) |
| 119 | + fun GetAudioVolume(handle: Long, outVolume: FloatArray): Int = nGetAudioVolume(handle, outVolume) |
| 120 | + fun GetAudioLevels(handle: Long, outLevels: FloatArray): Int = nGetAudioLevels(handle, outLevels) |
| 121 | + fun GetPlaybackSpeed(handle: Long, outSpeed: FloatArray): Int = nGetPlaybackSpeed(handle, outSpeed) |
| 122 | + fun SetOutputSize(handle: Long, width: Int, height: Int): Int = nSetOutputSize(handle, width, height) |
126 | 123 | } |
0 commit comments