Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,43 +1,78 @@
package io.github.kdroidfilter.composemediaplayer.mac

import com.sun.jna.Native
import com.sun.jna.Pointer
import java.io.File
import java.nio.ByteBuffer
import java.nio.file.Files

/**
* JNA direct mapping to the native library.
* Includes methods to retrieve frame rate and metadata information.
* JNI direct mapping to the native macOS video player library.
* Handles are opaque Long values (native pointer cast to jlong, 0 = null).
*/
internal object SharedVideoPlayer {
init {
// Register the native library for direct mapping
Native.register("NativeVideoPlayer")
loadNativeLibrary()
}

@JvmStatic external fun createVideoPlayer(): Pointer?
@JvmStatic external fun openUri(context: Pointer?, uri: String?)
@JvmStatic external fun playVideo(context: Pointer?)
@JvmStatic external fun pauseVideo(context: Pointer?)
@JvmStatic external fun setVolume(context: Pointer?, volume: Float)
@JvmStatic external fun getVolume(context: Pointer?): Float
@JvmStatic external fun getLatestFrame(context: Pointer?): Pointer?
@JvmStatic external fun getFrameWidth(context: Pointer?): Int
@JvmStatic external fun getFrameHeight(context: Pointer?): Int
@JvmStatic external fun getVideoFrameRate(context: Pointer?): Float
@JvmStatic external fun getScreenRefreshRate(context: Pointer?): Float
@JvmStatic external fun getCaptureFrameRate(context: Pointer?): Float
@JvmStatic external fun getVideoDuration(context: Pointer?): Double
@JvmStatic external fun getCurrentTime(context: Pointer?): Double
@JvmStatic external fun seekTo(context: Pointer?, time: Double)
@JvmStatic external fun disposeVideoPlayer(context: Pointer?)
@JvmStatic external fun getLeftAudioLevel(context: Pointer?): Float
@JvmStatic external fun getRightAudioLevel(context: Pointer?): Float
@JvmStatic external fun setPlaybackSpeed(context: Pointer?, speed: Float)
@JvmStatic external fun getPlaybackSpeed(context: Pointer?): Float

// Metadata retrieval functions
@JvmStatic external fun getVideoTitle(context: Pointer?): String?
@JvmStatic external fun getVideoBitrate(context: Pointer?): Long
@JvmStatic external fun getVideoMimeType(context: Pointer?): String?
@JvmStatic external fun getAudioChannels(context: Pointer?): Int
@JvmStatic external fun getAudioSampleRate(context: Pointer?): Int
private fun loadNativeLibrary() {
val osArch = System.getProperty("os.arch", "").lowercase()
val resourceDir =
if (osArch == "aarch64" || osArch == "arm64") "darwin-aarch64" else "darwin-x86-64"
val libName = "libNativeVideoPlayer.dylib"

val stream = SharedVideoPlayer::class.java.getResourceAsStream("/$resourceDir/$libName")
?: throw UnsatisfiedLinkError(
"Native library not found in resources: /$resourceDir/$libName"
)

val tempDir = Files.createTempDirectory("nativevideoplayer").toFile()
val tempFile = File(tempDir, libName)
stream.use { input -> tempFile.outputStream().use { input.copyTo(it) } }
System.load(tempFile.absolutePath)
tempFile.deleteOnExit()
tempDir.deleteOnExit()
}

// Playback control
@JvmStatic external fun nCreatePlayer(): Long
@JvmStatic external fun nOpenUri(handle: Long, uri: String)
@JvmStatic external fun nPlay(handle: Long)
@JvmStatic external fun nPause(handle: Long)
@JvmStatic external fun nSetVolume(handle: Long, volume: Float)
@JvmStatic external fun nGetVolume(handle: Long): Float
@JvmStatic external fun nSeekTo(handle: Long, time: Double)
@JvmStatic external fun nDisposePlayer(handle: Long)
@JvmStatic external fun nSetPlaybackSpeed(handle: Long, speed: Float)
@JvmStatic external fun nGetPlaybackSpeed(handle: Long): Float

// Frame access — lock/unlock CVPixelBuffer directly (zero intermediate copy)
// outInfo must be IntArray(3); filled with [width, height, bytesPerRow] on success.
// Returns the native base address of the locked buffer, or 0 on failure.
// MUST call nUnlockFrame after reading.
@JvmStatic external fun nLockFrame(handle: Long, outInfo: IntArray): Long
@JvmStatic external fun nUnlockFrame(handle: Long)
@JvmStatic external fun nWrapPointer(address: Long, size: Long): ByteBuffer?
@JvmStatic external fun nGetFrameWidth(handle: Long): Int
@JvmStatic external fun nGetFrameHeight(handle: Long): Int
@JvmStatic external fun nSetOutputSize(handle: Long, width: Int, height: Int): Int

// Timing / rate info
@JvmStatic external fun nGetVideoFrameRate(handle: Long): Float
@JvmStatic external fun nGetScreenRefreshRate(handle: Long): Float
@JvmStatic external fun nGetCaptureFrameRate(handle: Long): Float
@JvmStatic external fun nGetVideoDuration(handle: Long): Double
@JvmStatic external fun nGetCurrentTime(handle: Long): Double

// Audio levels
@JvmStatic external fun nGetLeftAudioLevel(handle: Long): Float
@JvmStatic external fun nGetRightAudioLevel(handle: Long): Float

// Metadata
@JvmStatic external fun nGetVideoTitle(handle: Long): String?
@JvmStatic external fun nGetVideoBitrate(handle: Long): Long
@JvmStatic external fun nGetVideoMimeType(handle: Long): String?
@JvmStatic external fun nGetAudioChannels(handle: Long): Int
@JvmStatic external fun nGetAudioSampleRate(handle: Long): Int

// Playback completion
@JvmStatic external fun nConsumeDidPlayToEnd(handle: Long): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@ internal fun copyBgraFrame(
dst: ByteBuffer,
width: Int,
height: Int,
srcBytesPerRow: Int,
dstRowBytes: Int,
) {
require(width > 0) { "width must be > 0 (was $width)" }
require(height > 0) { "height must be > 0 (was $height)" }
val srcRowBytes = width * 4
require(dstRowBytes >= srcRowBytes) {
"dstRowBytes ($dstRowBytes) must be >= srcRowBytes ($srcRowBytes)"
val pixelRowBytes = width * 4
require(srcBytesPerRow >= pixelRowBytes) {
"srcBytesPerRow ($srcBytesPerRow) must be >= pixelRowBytes ($pixelRowBytes)"
}
require(dstRowBytes >= pixelRowBytes) {
"dstRowBytes ($dstRowBytes) must be >= pixelRowBytes ($pixelRowBytes)"
}

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

if (dstRowBytes == srcRowBytes) {
srcBuf.limit(requiredSrcBytes.toInt())
dstBuf.limit(requiredSrcBytes.toInt())
// Fast path: both buffers have the same layout — single bulk copy
if (srcBytesPerRow == pixelRowBytes && dstRowBytes == pixelRowBytes) {
val totalBytes = pixelRowBytes.toLong() * height.toLong()
srcBuf.limit(totalBytes.toInt())
dstBuf.limit(totalBytes.toInt())
dstBuf.put(srcBuf)
return
}

// Slow path: different strides — copy row by row
val srcCapacity = srcBuf.capacity()
val dstCapacity = dstBuf.capacity()
for (row in 0 until height) {
val srcPos = row * srcRowBytes
val srcPos = row * srcBytesPerRow
srcBuf.limit(srcCapacity)
srcBuf.position(srcPos)
srcBuf.limit(srcPos + srcRowBytes)
srcBuf.limit(srcPos + pixelRowBytes)

val dstPos = row * dstRowBytes
dstBuf.limit(dstCapacity)
dstBuf.position(dstPos)
dstBuf.limit(dstPos + srcRowBytes)
dstBuf.limit(dstPos + pixelRowBytes)

dstBuf.put(srcBuf)
}
Expand Down
Loading
Loading