Skip to content

Commit b03b6f0

Browse files
authored
Merge pull request #173 from kdroidFilter/feature/hls-windows-support
Add HLS streaming support on Windows
2 parents def583b + 017bcfe commit b03b6f0

12 files changed

Lines changed: 2179 additions & 946 deletions

File tree

Lines changed: 107 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,123 @@
11
package io.github.kdroidfilter.composemediaplayer.windows
22

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
113
import io.github.kdroidfilter.composemediaplayer.VideoMetadata
4+
import java.io.File
5+
import java.nio.ByteBuffer
6+
import java.nio.file.Files
127

138
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
2011

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."
6518
}
6619
}
6720

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()
7636
}
7737

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
8443
}
8544

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+
)
9569
}
9670

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)
126123
}

0 commit comments

Comments
 (0)