Skip to content

Commit de423fc

Browse files
authored
Merge pull request #184 from kdroidFilter/feature/mac-jni-migration-and-output-scaling
Migrate macOS player from JNA to JNI with output scaling and performance improvements
2 parents 848f408 + d8011ca commit de423fc

8 files changed

Lines changed: 748 additions & 311 deletions

File tree

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,78 @@
11
package io.github.kdroidfilter.composemediaplayer.mac
22

3-
import com.sun.jna.Native
4-
import com.sun.jna.Pointer
3+
import java.io.File
4+
import java.nio.ByteBuffer
5+
import java.nio.file.Files
56

67
/**
7-
* JNA direct mapping to the native library.
8-
* Includes methods to retrieve frame rate and metadata information.
8+
* JNI direct mapping to the native macOS video player library.
9+
* Handles are opaque Long values (native pointer cast to jlong, 0 = null).
910
*/
1011
internal object SharedVideoPlayer {
1112
init {
12-
// Register the native library for direct mapping
13-
Native.register("NativeVideoPlayer")
13+
loadNativeLibrary()
1414
}
1515

16-
@JvmStatic external fun createVideoPlayer(): Pointer?
17-
@JvmStatic external fun openUri(context: Pointer?, uri: String?)
18-
@JvmStatic external fun playVideo(context: Pointer?)
19-
@JvmStatic external fun pauseVideo(context: Pointer?)
20-
@JvmStatic external fun setVolume(context: Pointer?, volume: Float)
21-
@JvmStatic external fun getVolume(context: Pointer?): Float
22-
@JvmStatic external fun getLatestFrame(context: Pointer?): Pointer?
23-
@JvmStatic external fun getFrameWidth(context: Pointer?): Int
24-
@JvmStatic external fun getFrameHeight(context: Pointer?): Int
25-
@JvmStatic external fun getVideoFrameRate(context: Pointer?): Float
26-
@JvmStatic external fun getScreenRefreshRate(context: Pointer?): Float
27-
@JvmStatic external fun getCaptureFrameRate(context: Pointer?): Float
28-
@JvmStatic external fun getVideoDuration(context: Pointer?): Double
29-
@JvmStatic external fun getCurrentTime(context: Pointer?): Double
30-
@JvmStatic external fun seekTo(context: Pointer?, time: Double)
31-
@JvmStatic external fun disposeVideoPlayer(context: Pointer?)
32-
@JvmStatic external fun getLeftAudioLevel(context: Pointer?): Float
33-
@JvmStatic external fun getRightAudioLevel(context: Pointer?): Float
34-
@JvmStatic external fun setPlaybackSpeed(context: Pointer?, speed: Float)
35-
@JvmStatic external fun getPlaybackSpeed(context: Pointer?): Float
36-
37-
// Metadata retrieval functions
38-
@JvmStatic external fun getVideoTitle(context: Pointer?): String?
39-
@JvmStatic external fun getVideoBitrate(context: Pointer?): Long
40-
@JvmStatic external fun getVideoMimeType(context: Pointer?): String?
41-
@JvmStatic external fun getAudioChannels(context: Pointer?): Int
42-
@JvmStatic external fun getAudioSampleRate(context: Pointer?): Int
16+
private fun loadNativeLibrary() {
17+
val osArch = System.getProperty("os.arch", "").lowercase()
18+
val resourceDir =
19+
if (osArch == "aarch64" || osArch == "arm64") "darwin-aarch64" else "darwin-x86-64"
20+
val libName = "libNativeVideoPlayer.dylib"
21+
22+
val stream = SharedVideoPlayer::class.java.getResourceAsStream("/$resourceDir/$libName")
23+
?: throw UnsatisfiedLinkError(
24+
"Native library not found in resources: /$resourceDir/$libName"
25+
)
26+
27+
val tempDir = Files.createTempDirectory("nativevideoplayer").toFile()
28+
val tempFile = File(tempDir, libName)
29+
stream.use { input -> tempFile.outputStream().use { input.copyTo(it) } }
30+
System.load(tempFile.absolutePath)
31+
tempFile.deleteOnExit()
32+
tempDir.deleteOnExit()
33+
}
34+
35+
// Playback control
36+
@JvmStatic external fun nCreatePlayer(): Long
37+
@JvmStatic external fun nOpenUri(handle: Long, uri: String)
38+
@JvmStatic external fun nPlay(handle: Long)
39+
@JvmStatic external fun nPause(handle: Long)
40+
@JvmStatic external fun nSetVolume(handle: Long, volume: Float)
41+
@JvmStatic external fun nGetVolume(handle: Long): Float
42+
@JvmStatic external fun nSeekTo(handle: Long, time: Double)
43+
@JvmStatic external fun nDisposePlayer(handle: Long)
44+
@JvmStatic external fun nSetPlaybackSpeed(handle: Long, speed: Float)
45+
@JvmStatic external fun nGetPlaybackSpeed(handle: Long): Float
46+
47+
// Frame access — lock/unlock CVPixelBuffer directly (zero intermediate copy)
48+
// outInfo must be IntArray(3); filled with [width, height, bytesPerRow] on success.
49+
// Returns the native base address of the locked buffer, or 0 on failure.
50+
// MUST call nUnlockFrame after reading.
51+
@JvmStatic external fun nLockFrame(handle: Long, outInfo: IntArray): Long
52+
@JvmStatic external fun nUnlockFrame(handle: Long)
53+
@JvmStatic external fun nWrapPointer(address: Long, size: Long): ByteBuffer?
54+
@JvmStatic external fun nGetFrameWidth(handle: Long): Int
55+
@JvmStatic external fun nGetFrameHeight(handle: Long): Int
56+
@JvmStatic external fun nSetOutputSize(handle: Long, width: Int, height: Int): Int
57+
58+
// Timing / rate info
59+
@JvmStatic external fun nGetVideoFrameRate(handle: Long): Float
60+
@JvmStatic external fun nGetScreenRefreshRate(handle: Long): Float
61+
@JvmStatic external fun nGetCaptureFrameRate(handle: Long): Float
62+
@JvmStatic external fun nGetVideoDuration(handle: Long): Double
63+
@JvmStatic external fun nGetCurrentTime(handle: Long): Double
64+
65+
// Audio levels
66+
@JvmStatic external fun nGetLeftAudioLevel(handle: Long): Float
67+
@JvmStatic external fun nGetRightAudioLevel(handle: Long): Float
68+
69+
// Metadata
70+
@JvmStatic external fun nGetVideoTitle(handle: Long): String?
71+
@JvmStatic external fun nGetVideoBitrate(handle: Long): Long
72+
@JvmStatic external fun nGetVideoMimeType(handle: Long): String?
73+
@JvmStatic external fun nGetAudioChannels(handle: Long): Int
74+
@JvmStatic external fun nGetAudioSampleRate(handle: Long): Int
75+
76+
// Playback completion
77+
@JvmStatic external fun nConsumeDidPlayToEnd(handle: Long): Boolean
4378
}

mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacFrameUtils.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@ internal fun copyBgraFrame(
1818
dst: ByteBuffer,
1919
width: Int,
2020
height: Int,
21+
srcBytesPerRow: Int,
2122
dstRowBytes: Int,
2223
) {
2324
require(width > 0) { "width must be > 0 (was $width)" }
2425
require(height > 0) { "height must be > 0 (was $height)" }
25-
val srcRowBytes = width * 4
26-
require(dstRowBytes >= srcRowBytes) {
27-
"dstRowBytes ($dstRowBytes) must be >= srcRowBytes ($srcRowBytes)"
26+
val pixelRowBytes = width * 4
27+
require(srcBytesPerRow >= pixelRowBytes) {
28+
"srcBytesPerRow ($srcBytesPerRow) must be >= pixelRowBytes ($pixelRowBytes)"
29+
}
30+
require(dstRowBytes >= pixelRowBytes) {
31+
"dstRowBytes ($dstRowBytes) must be >= pixelRowBytes ($pixelRowBytes)"
2832
}
2933

30-
val requiredSrcBytes = srcRowBytes.toLong() * height.toLong()
34+
val requiredSrcBytes = srcBytesPerRow.toLong() * height.toLong()
3135
val requiredDstBytes = dstRowBytes.toLong() * height.toLong()
3236
require(src.capacity().toLong() >= requiredSrcBytes) {
3337
"src buffer too small: ${src.capacity()} < $requiredSrcBytes"
@@ -41,25 +45,28 @@ internal fun copyBgraFrame(
4145
srcBuf.rewind()
4246
dstBuf.rewind()
4347

44-
if (dstRowBytes == srcRowBytes) {
45-
srcBuf.limit(requiredSrcBytes.toInt())
46-
dstBuf.limit(requiredSrcBytes.toInt())
48+
// Fast path: both buffers have the same layout — single bulk copy
49+
if (srcBytesPerRow == pixelRowBytes && dstRowBytes == pixelRowBytes) {
50+
val totalBytes = pixelRowBytes.toLong() * height.toLong()
51+
srcBuf.limit(totalBytes.toInt())
52+
dstBuf.limit(totalBytes.toInt())
4753
dstBuf.put(srcBuf)
4854
return
4955
}
5056

57+
// Slow path: different strides — copy row by row
5158
val srcCapacity = srcBuf.capacity()
5259
val dstCapacity = dstBuf.capacity()
5360
for (row in 0 until height) {
54-
val srcPos = row * srcRowBytes
61+
val srcPos = row * srcBytesPerRow
5562
srcBuf.limit(srcCapacity)
5663
srcBuf.position(srcPos)
57-
srcBuf.limit(srcPos + srcRowBytes)
64+
srcBuf.limit(srcPos + pixelRowBytes)
5865

5966
val dstPos = row * dstRowBytes
6067
dstBuf.limit(dstCapacity)
6168
dstBuf.position(dstPos)
62-
dstBuf.limit(dstPos + srcRowBytes)
69+
dstBuf.limit(dstPos + pixelRowBytes)
6370

6471
dstBuf.put(srcBuf)
6572
}

0 commit comments

Comments
 (0)