├── .gitignore ├── app ├── android │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── me │ │ └── mimao │ │ └── android │ │ └── MainActivity.kt ├── desktop │ ├── build.gradle.kts │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── Main.kt ├── macos │ ├── build.gradle.kts │ └── src │ │ └── macosMain │ │ └── kotlin │ │ └── main.kt ├── shared │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── mimao │ │ │ └── kmp │ │ │ └── videoplayer │ │ │ └── sample │ │ │ └── rememberVideoPlayerState.kt │ │ ├── commonMain │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── mimao │ │ │ │ └── kmp │ │ │ │ └── videoplayer │ │ │ │ └── sample │ │ │ │ ├── App.kt │ │ │ │ └── VideoPlayer.kt │ │ └── resources │ │ │ ├── image.png │ │ │ └── video.mp4 │ │ ├── darwinMain │ │ └── kotlin │ │ │ └── com │ │ │ └── mimao │ │ │ └── kmp │ │ │ └── videoplayer │ │ │ └── sample │ │ │ └── rememberVideoPlayerState.kt │ │ └── desktopMain │ │ └── kotlin │ │ └── com │ │ └── mimao │ │ └── kmp │ │ └── videoplayer │ │ └── sample │ │ └── rememberVideoPlayerState.kt └── uikit │ ├── README.md │ ├── build.gradle.kts │ ├── project.yml │ └── src │ └── uiKitMain │ └── kotlin │ └── com │ └── mimao │ └── kmp │ └── videoplayer │ └── sample │ └── main.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Versions.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kvideoplayer ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── mimao │ │ └── kmp │ │ └── videoplayer │ │ └── KVideoPlayer.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── mimao │ │ └── kmp │ │ └── videoplayer │ │ └── KVideoPlayer.kt │ ├── darwinMain │ └── kotlin │ │ └── com.mimao.kmp.videoplayer │ │ └── KVideoPlayer.darwin.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── mimao │ │ └── kmp │ │ └── videoplayer │ │ └── KVideoPlayer.desktop.kt │ ├── iosMain │ └── kotlin │ │ └── com.mimao.kmp.videoplayer │ │ └── KVideoPlayerExt.ios.kt │ ├── macosMain │ └── kotlin │ │ └── com.mimao.kmp.videoplayer │ │ └── KVideoPlayerExt.macos.kt │ └── nativeInterop │ └── cinterop │ └── observer.def └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Android Profiling 88 | *.hprof 89 | 90 | */.idea 91 | .idea/ 92 | 93 | signing.properties 94 | 95 | *.jks 96 | 97 | .DS_Store 98 | 99 | *.xcodeproj/ 100 | plists/ -------------------------------------------------------------------------------- /app/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.compose") version Versions.compose 3 | id("com.android.application") 4 | kotlin("android") 5 | } 6 | 7 | group = "me.mimao" 8 | version = "1.0" 9 | 10 | dependencies { 11 | implementation(projects.app.shared) 12 | implementation("androidx.activity:activity-compose:1.4.0") 13 | } 14 | 15 | android { 16 | compileSdk = 32 17 | defaultConfig { 18 | applicationId = "me.mimao.android" 19 | minSdk = 24 20 | targetSdk = 32 21 | versionCode = 1 22 | versionName = "1.0" 23 | } 24 | compileOptions { 25 | sourceCompatibility = Versions.Java.java 26 | targetCompatibility = Versions.Java.java 27 | } 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/android/src/main/java/me/mimao/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.mimao.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.mimao.kmp.videoplayer.sample.App 7 | 8 | class MainActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContent { 12 | App() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /app/desktop/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.compose 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | id("org.jetbrains.compose") version Versions.compose 7 | } 8 | 9 | group = "me.mimao" 10 | version = "1.0" 11 | 12 | kotlin { 13 | jvm { 14 | compilations.all { 15 | kotlinOptions.jvmTarget = Versions.Java.jvmTarget 16 | } 17 | withJava() 18 | } 19 | sourceSets { 20 | val jvmMain by getting { 21 | dependencies { 22 | implementation(projects.app.shared) 23 | implementation(compose.desktop.currentOs) 24 | } 25 | } 26 | val jvmTest by getting 27 | } 28 | } 29 | 30 | compose.desktop { 31 | application { 32 | mainClass = "MainKt" 33 | nativeDistributions { 34 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 35 | packageName = "com.mimao.sample.MainKt" 36 | packageVersion = "1.0.0" 37 | macOS { 38 | bundleID = "com.mimao.sample.MainKt" 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/desktop/src/jvmMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material.MaterialTheme 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import com.mimao.kmp.videoplayer.sample.App 5 | 6 | fun main() = application { 7 | Window(onCloseRequest = ::exitApplication) { 8 | MaterialTheme { 9 | App() 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /app/macos/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | id("org.jetbrains.compose") version Versions.compose 7 | } 8 | 9 | kotlin { 10 | macosX64 { 11 | binaries { 12 | executable { 13 | entryPoint = "main" 14 | freeCompilerArgs += listOf( 15 | "-linker-option", "-framework", "-linker-option", "Metal" 16 | ) 17 | } 18 | } 19 | } 20 | macosArm64 { 21 | binaries { 22 | executable { 23 | entryPoint = "main" 24 | freeCompilerArgs += listOf( 25 | "-linker-option", "-framework", "-linker-option", "Metal" 26 | ) 27 | } 28 | } 29 | } 30 | 31 | sourceSets { 32 | val macosMain by creating { 33 | dependencies { 34 | implementation(projects.app.shared) 35 | } 36 | } 37 | val macosX64Main by getting { 38 | dependsOn(macosMain) 39 | } 40 | val macosArm64Main by getting { 41 | dependsOn(macosMain) 42 | } 43 | } 44 | } 45 | 46 | compose.desktop.nativeApplication { 47 | targets(kotlin.targets.getByName("macosX64"), kotlin.targets.getByName("macosArm64")) 48 | distributions { 49 | targetFormats(org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg) 50 | packageName = "KmpVideoPlayer" 51 | packageVersion = "1.0.0" 52 | macOS { 53 | bundleID = "com.mimao.kmp.videoplayer.sample" 54 | } 55 | } 56 | } 57 | 58 | tasks.withType { 59 | kotlinOptions.jvmTarget = Versions.Java.jvmTarget 60 | } 61 | 62 | kotlin { 63 | targets.withType { 64 | binaries.all { 65 | // TODO: the current compose binary surprises LLVM, so disable checks for now. 66 | freeCompilerArgs += "-Xdisable-phases=VerifyBitcode" 67 | binaryOptions["memoryModel"] = "experimental" 68 | } 69 | } 70 | } 71 | 72 | 73 | if (System.getProperty("os.arch") == "aarch64") { 74 | val runMacos by tasks.registering { 75 | dependsOn("runDebugExecutableMacosArm64") 76 | } 77 | val runMacosRelease by tasks.registering { 78 | dependsOn("runReleaseExecutableMacosArm64") 79 | } 80 | } else { 81 | val runMacos by tasks.registering { 82 | dependsOn("runDebugExecutableMacosX64") 83 | } 84 | val runMacosRelease by tasks.registering { 85 | dependsOn("runReleaseExecutableMacosX64") 86 | } 87 | } -------------------------------------------------------------------------------- /app/macos/src/macosMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import com.mimao.kmp.videoplayer.createKVideoPlayerWithView 3 | import platform.AVKit.AVPlayerView 4 | import platform.AppKit.* 5 | import platform.Foundation.NSMakeRect 6 | import platform.darwin.NSObject 7 | 8 | fun main() { 9 | val app = NSApplication.sharedApplication() 10 | app.delegate = object : NSObject(), NSApplicationDelegateProtocol { 11 | override fun applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication): Boolean { 12 | return true 13 | } 14 | } 15 | val window = object : NSWindow( 16 | contentRect = NSMakeRect(0.0, 0.0, 640.0, 780.0), 17 | styleMask = NSWindowStyleMaskTitled or 18 | NSWindowStyleMaskMiniaturizable or 19 | NSWindowStyleMaskClosable or 20 | NSWindowStyleMaskResizable, 21 | backing = NSBackingStoreBuffered, 22 | defer = false 23 | ){ 24 | override fun canBecomeKeyWindow(): Boolean { 25 | return true 26 | } 27 | 28 | override fun canBecomeMainWindow(): Boolean { 29 | return true 30 | } 31 | } 32 | 33 | val playerView = AVPlayerView(NSMakeRect(0.0, 0.0, 640.0, 480.0)) 34 | 35 | createKVideoPlayerWithView(playerView).apply { 36 | prepare("https://www.w3schools.com/html/movie.mp4") 37 | setRepeat(false) 38 | setVolume(1f) 39 | } 40 | window.contentView!!.addSubview(playerView) 41 | window.makeFirstResponder(playerView) 42 | window.makeKeyAndOrderFront(app) 43 | app.run() 44 | } 45 | 46 | -------------------------------------------------------------------------------- /app/shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.compose 2 | plugins { 3 | kotlin("multiplatform") 4 | id("com.android.library") 5 | id("org.jetbrains.compose") version Versions.compose 6 | } 7 | 8 | kotlin { 9 | android() 10 | jvm("desktop") { 11 | compilations.all { 12 | kotlinOptions.jvmTarget = Versions.Java.jvmTarget 13 | } 14 | } 15 | iosX64() 16 | iosArm64() 17 | macosX64() 18 | macosArm64() 19 | 20 | sourceSets { 21 | val commonMain by getting { 22 | dependencies { 23 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2") 24 | api(compose.ui) 25 | api(compose.runtime) 26 | api(compose.foundation) 27 | api(compose.material) 28 | api(project(":kvideoplayer")) 29 | } 30 | } 31 | 32 | val androidMain by getting { 33 | dependencies { 34 | api("androidx.appcompat:appcompat:1.4.2") 35 | api("androidx.core:core-ktx:1.8.0") 36 | api("androidx.compose.material3:material3:${Versions.android_material3}") 37 | } 38 | } 39 | val desktopMain by getting { 40 | dependencies { 41 | implementation(compose.material) 42 | } 43 | } 44 | val darwinMain by creating { 45 | dependsOn(commonMain) 46 | } 47 | 48 | val iosArm64Main by getting { 49 | dependsOn(darwinMain) 50 | } 51 | val iosX64Main by getting { 52 | dependsOn(darwinMain) 53 | } 54 | val macosX64Main by getting { 55 | dependsOn(darwinMain) 56 | } 57 | val macosArm64Main by getting { 58 | dependsOn(darwinMain) 59 | } 60 | } 61 | } 62 | 63 | android { 64 | compileSdk = 32 65 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 66 | defaultConfig { 67 | minSdk = 24 68 | targetSdk = 32 69 | } 70 | compileOptions { 71 | sourceCompatibility = Versions.Java.java 72 | targetCompatibility = Versions.Java.java 73 | } 74 | } -------------------------------------------------------------------------------- /app/shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/shared/src/androidMain/kotlin/com/mimao/kmp/videoplayer/sample/rememberVideoPlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.viewinterop.AndroidView 7 | import com.google.android.exoplayer2.ui.StyledPlayerView 8 | import com.mimao.kmp.videoplayer.KVideoPlayer 9 | 10 | @Composable 11 | actual fun rememberVideoPlayerState(): VideoPlayerState { 12 | val context = LocalContext.current 13 | return remember { 14 | val view = StyledPlayerView(context) 15 | VideoPlayerState( 16 | player = KVideoPlayer(view), 17 | content = { 18 | AndroidView( 19 | factory = { 20 | view 21 | }, 22 | modifier = it 23 | ) 24 | } 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /app/shared/src/commonMain/kotlin/com/mimao/kmp/videoplayer/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.* 5 | import androidx.compose.runtime.* 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import com.mimao.kmp.videoplayer.KPlayerStatus 10 | 11 | @Composable 12 | fun App() { 13 | val (player, videoLayout) = rememberVideoPlayerState() 14 | LaunchedEffect(player) { 15 | player.apply { 16 | prepare("https://video.twimg.com/amplify_video/1589178626284847104/vid/1280x720/7ctjE5yWg6XaDE_T.mp4?tag=14") 17 | } 18 | } 19 | val status by player.status.collectAsState(KPlayerStatus.Idle) 20 | val volume by player.volume.collectAsState(1f) 21 | val isMuted by player.isMute.collectAsState(false) 22 | val duration by player.duration.collectAsState(0L) 23 | val currentTime by player.currentTime.collectAsState(0L) 24 | val isRepeated by player.isRepeated.collectAsState(false) 25 | 26 | println("status: $status, $volume, $isMuted, $currentTime, $duration") 27 | var seek:Float by remember { mutableStateOf(0f) } 28 | var seeking: Boolean by remember { mutableStateOf(false) } 29 | Column { 30 | videoLayout.invoke(Modifier.fillMaxWidth().height(300.dp)) 31 | Row(modifier = Modifier.fillMaxWidth()) { 32 | Slider( 33 | value = if (duration > 0 && !seeking) currentTime / duration.toFloat() else seek, 34 | modifier = Modifier.weight(1f), 35 | onValueChange = { 36 | seek = it 37 | seeking = true 38 | }, 39 | onValueChangeFinished = { 40 | player.seekTo((duration * seek).toLong()) 41 | seeking = false 42 | } 43 | ) 44 | } 45 | Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { 46 | if (status is KPlayerStatus.Error) { 47 | Button(onClick = { 48 | player.play() 49 | }) { 50 | Text("Reload") 51 | } 52 | } else if (status is KPlayerStatus.Buffering) { 53 | CircularProgressIndicator() 54 | } else { 55 | Button(onClick = { 56 | when (status) { 57 | KPlayerStatus.Playing -> player.pause() 58 | else -> player.play() 59 | } 60 | }) { 61 | when (status) { 62 | KPlayerStatus.Playing -> Text("Pause") 63 | else -> Text("Play") 64 | } 65 | } 66 | } 67 | 68 | Button(onClick = { 69 | player.setRepeat(!isRepeated) 70 | }) { 71 | Text("Mode") 72 | } 73 | 74 | if (status is KPlayerStatus.Error) { 75 | Text("Error: ${(status as KPlayerStatus.Error).error}") 76 | } 77 | 78 | if (isMuted) { 79 | Text("Volume: Muted!") 80 | } else { 81 | Text("Volume: ${volume * 100}%") 82 | } 83 | 84 | Text(if (isRepeated) "Repeat: On" else "Repeat: Off") 85 | 86 | } 87 | Row( 88 | modifier = Modifier.fillMaxWidth(), 89 | verticalAlignment = Alignment.CenterVertically, 90 | horizontalArrangement = Arrangement.SpaceBetween 91 | ) { 92 | Button(onClick = { 93 | player.setMute(!isMuted) 94 | }) { 95 | Text(if (isMuted) "unMute" else "Mute") 96 | } 97 | Button(onClick = { 98 | player.setVolume(volume - 0.1f) 99 | }) { 100 | Text("volume -10%") 101 | } 102 | 103 | Button(onClick = { 104 | player.setVolume(volume + 0.1f) 105 | }) { 106 | Text("volume +10%") 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /app/shared/src/commonMain/kotlin/com/mimao/kmp/videoplayer/sample/VideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.mimao.kmp.videoplayer.KVideoPlayer 6 | 7 | @Composable 8 | expect fun rememberVideoPlayerState(): VideoPlayerState 9 | 10 | data class VideoPlayerState( 11 | val player: KVideoPlayer, 12 | val content: @Composable (Modifier) -> Unit 13 | ) -------------------------------------------------------------------------------- /app/shared/src/commonMain/resources/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeSteven/KMP-VideoPlayer/4e19d34e1327ea3f5874a1ee39293ffccbfa78d4/app/shared/src/commonMain/resources/image.png -------------------------------------------------------------------------------- /app/shared/src/commonMain/resources/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeSteven/KMP-VideoPlayer/4e19d34e1327ea3f5874a1ee39293ffccbfa78d4/app/shared/src/commonMain/resources/video.mp4 -------------------------------------------------------------------------------- /app/shared/src/darwinMain/kotlin/com/mimao/kmp/videoplayer/sample/rememberVideoPlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.Modifier 7 | import com.mimao.kmp.videoplayer.KVideoPlayer 8 | 9 | @Composable 10 | actual fun rememberVideoPlayerState(): VideoPlayerState { 11 | return remember { 12 | val player = KVideoPlayer() 13 | VideoPlayerState( 14 | player = player, 15 | content = { 16 | IosVideoRender(it, player) 17 | } 18 | ) 19 | } 20 | 21 | } 22 | 23 | @Composable 24 | fun IosVideoRender(modifier: Modifier, player: KVideoPlayer) { 25 | Text(modifier = modifier, text = "haven't support compose yet") 26 | } -------------------------------------------------------------------------------- /app/shared/src/desktopMain/kotlin/com/mimao/kmp/videoplayer/sample/rememberVideoPlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.awt.SwingPanel 6 | import androidx.compose.ui.graphics.Color 7 | import com.mimao.kmp.videoplayer.KVideoPlayer 8 | import com.mimao.kmp.videoplayer.defaultComponent 9 | import org.jetbrains.skia.Image 10 | import java.awt.Component 11 | 12 | @Composable 13 | actual fun rememberVideoPlayerState(): VideoPlayerState { 14 | return remember { 15 | val component = defaultComponent() 16 | VideoPlayerState( 17 | player = KVideoPlayer(component), 18 | content = { 19 | SwingPanel( 20 | background = Color.Transparent, 21 | factory = { 22 | component as Component 23 | }, 24 | modifier = it 25 | ) 26 | } 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /app/uikit/README.md: -------------------------------------------------------------------------------- 1 | # How to build 2 | 3 | ## Requirement 4 | 5 | - xcode 6 | - xcodegen 7 | 8 | ## Build step 9 | 10 | - run `xcodegen generate` 11 | - open `Kmp VideoPlayer.xcodeproj` in xcode 12 | - click run in xcode with some simulator 13 | - Doesn't work with real device and i have no idea why -------------------------------------------------------------------------------- /app/uikit/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("org.jetbrains.compose") version Versions.compose 6 | } 7 | 8 | kotlin { 9 | iosX64("uikitX64") { 10 | binaries { 11 | executable { 12 | entryPoint = "com.mimao.kmp.videoplayer.sample.main" 13 | freeCompilerArgs = freeCompilerArgs + listOf( 14 | "-linker-option", "-framework", "-linker-option", "Metal", 15 | "-linker-option", "-framework", "-linker-option", "CoreText", 16 | "-linker-option", "-framework", "-linker-option", "CoreGraphics" 17 | ) 18 | } 19 | } 20 | } 21 | iosArm64("uikitArm64") { 22 | binaries { 23 | executable { 24 | entryPoint = "com.mimao.kmp.videoplayer.sample" 25 | freeCompilerArgs = freeCompilerArgs + listOf( 26 | "-linker-option", "-framework", "-linker-option", "Metal", 27 | "-linker-option", "-framework", "-linker-option", "CoreText", 28 | "-linker-option", "-framework", "-linker-option", "CoreGraphics" 29 | ) 30 | // TODO: the current compose binary surprises LLVM, so disable checks for now. 31 | freeCompilerArgs = freeCompilerArgs + "-Xdisable-phases=VerifyBitcode" 32 | } 33 | } 34 | } 35 | sourceSets { 36 | val uikitMain by creating { 37 | dependencies { 38 | implementation(projects.app.shared) 39 | } 40 | } 41 | val uikitX64Main by getting { 42 | dependsOn(uikitMain) 43 | } 44 | val uikitArm64Main by getting { 45 | dependsOn(uikitMain) 46 | } 47 | } 48 | } 49 | 50 | compose.experimental { 51 | uikit.application { 52 | bundleIdPrefix = "com.mimao.kmp.videoplayer.sample" 53 | projectName = "Kmp VideoPlayer" 54 | } 55 | } 56 | 57 | kotlin { 58 | targets.withType { 59 | binaries.all { 60 | // TODO: the current compose binary surprises LLVM, so disable checks for now. 61 | freeCompilerArgs = freeCompilerArgs + "-Xdisable-phases=VerifyBitcode" 62 | } 63 | } 64 | } 65 | 66 | // TODO: remove when https://youtrack.jetbrains.com/issue/KT-50778 fixed 67 | project.tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile::class.java).configureEach { 68 | kotlinOptions.freeCompilerArgs += listOf( 69 | "-Xir-dce-runtime-diagnostic=log" 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /app/uikit/project.yml: -------------------------------------------------------------------------------- 1 | name: Kmp VideoPlayer 2 | options: 3 | bundleIdPrefix: com.mimao.kmp.videoplayer.sample 4 | settings: 5 | DEVELOPMENT_TEAM: N462MKSJ7M 6 | CODE_SIGN_IDENTITY: "iPhone Developer" 7 | CODE_SIGN_STYLE: Automatic 8 | MARKETING_VERSION: "1.0" 9 | CURRENT_PROJECT_VERSION: "4" 10 | SDKROOT: iphoneos 11 | targets: 12 | KMP-VideoPlayer: 13 | type: application 14 | platform: iOS 15 | deploymentTarget: "12.0" 16 | prebuildScripts: 17 | - script: cd "$SRCROOT" && ../../gradlew -i -p . packComposeUikitApplicationForXCode 18 | name: GradleCompile 19 | info: 20 | path: plists/Ios/Info.plist 21 | properties: 22 | UILaunchStoryboardName: "" 23 | sources: 24 | - "src/" 25 | settings: 26 | LIBRARY_SEARCH_PATHS: "$(inherited)" 27 | ENABLE_BITCODE: "YES" 28 | ONLY_ACTIVE_ARCH: "NO" 29 | VALID_ARCHS: "arm64" -------------------------------------------------------------------------------- /app/uikit/src/uiKitMain/kotlin/com/mimao/kmp/videoplayer/sample/main.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer.sample 2 | 3 | import androidx.compose.ui.window.Application 4 | import com.mimao.kmp.videoplayer.createKVideoPlayerWithController 5 | import kotlinx.cinterop.* 6 | import org.jetbrains.skia.Canvas 7 | import org.jetbrains.skia.Color 8 | import org.jetbrains.skia.Paint 9 | import org.jetbrains.skiko.* 10 | import platform.AVKit.AVPlayerViewController 11 | import platform.Foundation.NSStringFromClass 12 | import platform.UIKit.* 13 | 14 | fun main() { 15 | val args = emptyArray() 16 | memScoped { 17 | val argc = args.size + 1 18 | val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues() 19 | autoreleasepool { 20 | UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate)) 21 | } 22 | } 23 | } 24 | 25 | class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol { 26 | companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta 27 | 28 | @ObjCObjectBase.OverrideInit 29 | constructor() : super() 30 | 31 | private var _window: UIWindow? = null 32 | override fun window() = _window 33 | override fun setWindow(window: UIWindow?) { 34 | _window = window 35 | } 36 | 37 | override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map?): Boolean { 38 | window = UIWindow(frame = UIScreen.mainScreen.bounds) 39 | window!!.rootViewController = VideoApp() 40 | window!!.makeKeyAndVisible() 41 | return true 42 | } 43 | } 44 | 45 | private fun composeApp() = Application("KMP Video Player") { 46 | App() 47 | } 48 | 49 | private fun SkikoApp() = SkikoViewController( 50 | SkikoUIView( 51 | SkiaLayer().apply { 52 | gesturesToListen = SkikoGestureEventKind.values() 53 | skikoView = GenericSkikoView(this, object : SkikoView { 54 | val paint = Paint().apply { color = Color.RED } 55 | override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { 56 | canvas.clear(Color.CYAN) 57 | val ts = nanoTime / 5_000_000 58 | canvas.drawCircle((ts % width).toFloat(), (ts % height).toFloat(), 20f, paint) 59 | } 60 | }) 61 | } 62 | ) 63 | ) 64 | 65 | private fun VideoApp() = AVPlayerViewController().apply { 66 | val kPlayer = createKVideoPlayerWithController(this) 67 | kPlayer.apply { 68 | prepare("https://www.w3schools.com/html/movie.mp4") 69 | setRepeat(true) 70 | setVolume(1f) 71 | } 72 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 2 | import com.vanniktech.maven.publish.SonatypeHost 3 | plugins { 4 | id("com.android.application").apply(false) 5 | id("com.android.library").apply(false) 6 | kotlin("android").apply(false) 7 | id("com.vanniktech.maven.publish") version "0.21.0" apply false 8 | } 9 | 10 | allprojects { 11 | tasks.withType { 12 | kotlinOptions { 13 | jvmTarget = Versions.Java.jvmTarget 14 | // allWarningsAsErrors = true 15 | freeCompilerArgs = freeCompilerArgs + listOf( 16 | "-opt-in=kotlin.RequiresOptIn", 17 | "-Xcontext-receivers", 18 | "-Xskip-prerelease-check", 19 | ) 20 | } 21 | } 22 | 23 | plugins.withId("com.vanniktech.maven.publish.base") { 24 | @Suppress("UnstableApiUsage") 25 | configure { 26 | publishToMavenCentral(SonatypeHost.S01) 27 | signAllPublications() 28 | pom { 29 | group = "io.github.joesteven" 30 | version = "1.0.1-dev03" 31 | name.set("kvideoplayer") 32 | description.set("Video player for Kotlin multiplatform") 33 | url.set("https://github.com/JoeSteven/KMP-VideoPlayer/") 34 | licenses { 35 | license { 36 | name.set("MIT") 37 | url.set("https://opensource.org/licenses/MIT") 38 | distribution.set("repo") 39 | } 40 | } 41 | developers { 42 | developer { 43 | id.set("Mimao") 44 | name.set("Mimao") 45 | email.set("qiaoxiaoxi621@gmail.com") 46 | } 47 | } 48 | scm { 49 | url.set("https://github.com/JoeSteven/KMP-VideoPlayer/") 50 | connection.set("scm:git:git://github.com/JoeSteven/KMP-VideoPlayer.git") 51 | developerConnection.set("scm:git:git://github.com/JoeSteven/KMP-VideoPlayer.git") 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | google() 8 | } 9 | 10 | dependencies { 11 | implementation("com.android.tools.build:gradle:7.3.1") 12 | api(kotlin("gradle-plugin", version = "1.7.20")) 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Versions.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.JavaVersion 2 | 3 | object Versions { 4 | const val compose = "1.2.0" 5 | 6 | object Kotlin { 7 | const val lang = "1.7.20" 8 | const val coroutines = "1.6.4" 9 | } 10 | 11 | object Java { 12 | const val jvmTarget = "11" 13 | val java = JavaVersion.VERSION_11 14 | } 15 | 16 | const val android_material3 = "1.0.0-alpha13" 17 | 18 | const val exoPlayer = "2.18.1" 19 | const val vlcj = "4.7.1" 20 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | kotlin.native.binary.memoryModel=experimental 25 | kotlin.native.cacheKind=none 26 | kotlin.mpp.enableCInteropCommonization=true 27 | android.disableAutomaticComponentCreation=true 28 | org.jetbrains.compose.experimental.uikit.enabled=true 29 | org.jetbrains.compose.experimental.macos.enabled=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeSteven/KMP-VideoPlayer/4e19d34e1327ea3f5874a1ee39293ffccbfa78d4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /kvideoplayer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.KotlinMultiplatform 2 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | id("com.android.library") 7 | id("com.vanniktech.maven.publish.base") 8 | } 9 | 10 | kotlin { 11 | ios { 12 | compilations.getByName("main") { 13 | cinterops { 14 | val observer by creating { 15 | defFile(project.file("src/nativeInterop/cinterop/observer.def")) 16 | packageName("com.mimao.kmp.videoplayer") 17 | } 18 | } 19 | } 20 | } 21 | iosArm64() 22 | iosX64() 23 | macosX64 { 24 | compilations.getByName("main") { 25 | cinterops { 26 | val observer by creating { 27 | defFile(project.file("src/nativeInterop/cinterop/observer.def")) 28 | packageName("com.mimao.kmp.videoplayer") 29 | } 30 | } 31 | } 32 | } 33 | macosArm64 { 34 | compilations.getByName("main") { 35 | cinterops { 36 | val observer by creating { 37 | defFile(project.file("src/nativeInterop/cinterop/observer.def")) 38 | packageName("com.mimao.kmp.videoplayer") 39 | } 40 | } 41 | } 42 | } 43 | android { 44 | publishLibraryVariants("debug", "release") 45 | } 46 | jvm("desktop") { 47 | compilations.all { 48 | kotlinOptions.jvmTarget = Versions.Java.jvmTarget 49 | } 50 | } 51 | sourceSets { 52 | val commonMain by getting { 53 | dependencies { 54 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.Kotlin.coroutines}") 55 | } 56 | } 57 | 58 | val androidMain by getting { 59 | dependencies { 60 | api("com.google.android.exoplayer:exoplayer:${Versions.exoPlayer}") 61 | api("com.google.android.exoplayer:extension-okhttp:${Versions.exoPlayer}") 62 | } 63 | } 64 | val desktopMain by getting { 65 | dependencies { 66 | api("uk.co.caprica:vlcj:4.7.1") 67 | } 68 | } 69 | val darwinMain by creating { 70 | dependsOn(commonMain) 71 | } 72 | val iosMain by getting{ 73 | dependsOn(darwinMain) 74 | } 75 | val iosArm64Main by getting { 76 | dependsOn(iosMain) 77 | } 78 | val iosX64Main by getting { 79 | dependsOn(iosMain) 80 | } 81 | val macosMain by creating { 82 | dependencies { 83 | dependsOn(darwinMain) 84 | } 85 | } 86 | val macosX64Main by getting { 87 | dependsOn(macosMain) 88 | } 89 | 90 | val macosArm64Main by getting { 91 | dependsOn(macosMain) 92 | } 93 | 94 | } 95 | } 96 | 97 | android { 98 | namespace = "io.github.joesteven.kvideoplayer" 99 | compileSdk = 33 100 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 101 | defaultConfig { 102 | minSdk = 24 103 | targetSdk = 33 104 | } 105 | compileOptions { 106 | sourceCompatibility = Versions.Java.java 107 | targetCompatibility = Versions.Java.java 108 | } 109 | } 110 | 111 | @Suppress("UnstableApiUsage") 112 | configure { 113 | configure(KotlinMultiplatform()) 114 | } -------------------------------------------------------------------------------- /kvideoplayer/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kvideoplayer/src/androidMain/kotlin/com/mimao/kmp/videoplayer/KVideoPlayer.android.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | 3 | import com.google.android.exoplayer2.ExoPlayer 4 | import com.google.android.exoplayer2.MediaItem 5 | import com.google.android.exoplayer2.PlaybackException 6 | import com.google.android.exoplayer2.Player 7 | import com.google.android.exoplayer2.ui.StyledPlayerView 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.Job 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.launch 15 | 16 | actual class KVideoPlayer( 17 | private val playerView: StyledPlayerView, 18 | ) { 19 | private val _status = MutableStateFlow(KPlayerStatus.Idle) 20 | actual val status: Flow 21 | get() = _status 22 | 23 | private val _volume = MutableStateFlow(1f) 24 | actual val volume: Flow 25 | get() = _volume 26 | 27 | private val _isMute = MutableStateFlow(false) 28 | actual val isMute: Flow 29 | get() = _isMute 30 | 31 | private val _currentTime = MutableStateFlow(0L) 32 | actual val currentTime: Flow 33 | get() = _currentTime 34 | 35 | private val _duration = MutableStateFlow(0L) 36 | actual val duration: Flow 37 | get() = _duration 38 | 39 | private val _isRepeated = MutableStateFlow(false) 40 | actual val isRepeated: Flow 41 | get() = _isRepeated 42 | 43 | private var countingJob: Job? = null 44 | private val scope = CoroutineScope(Dispatchers.Main) 45 | private var currentDataSource: Any? = null 46 | private val listener = object : Player.Listener { 47 | override fun onPlaybackStateChanged(playbackState: Int) { 48 | when (playbackState) { 49 | Player.STATE_IDLE -> if (_status.value !is KPlayerStatus.Error) _status.value = KPlayerStatus.Idle 50 | Player.STATE_BUFFERING -> _status.value = KPlayerStatus.Buffering 51 | Player.STATE_READY -> { 52 | _status.value = KPlayerStatus.Ready 53 | emitDuration() 54 | } 55 | 56 | Player.STATE_ENDED -> _status.value = KPlayerStatus.Ended 57 | else -> {} 58 | } 59 | } 60 | 61 | override fun onIsPlayingChanged(isPlaying: Boolean) { 62 | if (isPlaying) _status.value = KPlayerStatus.Playing 63 | countingJob?.cancel() 64 | if (isPlaying) { 65 | countingJob = scope.launch { 66 | while (true) { 67 | delay(1000) 68 | _currentTime.value = playerView.player?.currentPosition ?: 0 69 | } 70 | } 71 | } 72 | } 73 | 74 | override fun onPlayerError(error: PlaybackException) { 75 | _status.value = KPlayerStatus.Error(error) 76 | } 77 | } 78 | 79 | actual fun prepare(dataSource: Any, playWhenReady: Boolean) { 80 | currentDataSource = dataSource 81 | _status.value = KPlayerStatus.Preparing 82 | playerView.apply { 83 | useController = false 84 | player?.release() 85 | player = ExoPlayer.Builder(context) 86 | .build() 87 | .apply { 88 | addListener(listener) 89 | setMediaItem(MediaItem.fromUri(dataSource as String)) 90 | this.playWhenReady = playWhenReady 91 | prepare() 92 | emitDuration() 93 | } 94 | } 95 | } 96 | 97 | actual fun play() { 98 | if (_status.value is KPlayerStatus.Error) { 99 | currentDataSource?.let { 100 | prepare(dataSource = it, playWhenReady = true) 101 | } 102 | } else { 103 | playerView.player?.playWhenReady = true 104 | playerView.onResume() 105 | } 106 | } 107 | 108 | actual fun pause() { 109 | playerView.player?.playWhenReady = false 110 | playerView.onPause() 111 | _status.value = KPlayerStatus.Paused 112 | } 113 | 114 | actual fun stop() { 115 | playerView.player?.stop() 116 | _status.value = KPlayerStatus.Paused 117 | } 118 | 119 | actual fun release() { 120 | countingJob?.cancel() 121 | playerView.player?.release() 122 | _status.value = KPlayerStatus.Released 123 | } 124 | 125 | actual fun seekTo(position: Long) { 126 | playerView.player?.seekTo(position) 127 | playerView.player?.currentPosition?.let { 128 | _currentTime.value = it 129 | } 130 | } 131 | 132 | actual fun setMute(mute: Boolean) { 133 | playerView.player?.volume = if (mute) 0f else _volume.value 134 | _isMute.value = mute 135 | } 136 | 137 | actual fun setVolume(volume: Float) { 138 | volume.coerceIn(0f, 1f).let { 139 | playerView.player?.volume = it 140 | _volume.value = it 141 | } 142 | } 143 | 144 | actual fun setRepeat(isRepeat: Boolean) { 145 | playerView.player?.repeatMode = if (isRepeat) ExoPlayer.REPEAT_MODE_ALL else ExoPlayer.REPEAT_MODE_OFF 146 | _isRepeated.value = isRepeat 147 | } 148 | 149 | private fun emitDuration() { 150 | _duration.value = playerView?.player?.duration ?: 0 151 | } 152 | } -------------------------------------------------------------------------------- /kvideoplayer/src/commonMain/kotlin/com/mimao/kmp/videoplayer/KVideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | expect class KVideoPlayer { 6 | val status: Flow 7 | val volume: Flow 8 | val isMute: Flow 9 | val currentTime: Flow 10 | val duration: Flow 11 | val isRepeated: Flow 12 | fun prepare(dataSource: Any, playWhenReady: Boolean = true) 13 | fun play() 14 | fun pause() 15 | fun stop() 16 | fun release() 17 | fun seekTo(position: Long) 18 | fun setMute(mute: Boolean) 19 | fun setVolume(volume: Float) 20 | fun setRepeat(isRepeat: Boolean) 21 | } 22 | 23 | sealed interface KPlayerStatus { 24 | object Idle : KPlayerStatus 25 | object Preparing : KPlayerStatus 26 | object Ready : KPlayerStatus 27 | object Buffering : KPlayerStatus 28 | object Playing : KPlayerStatus 29 | object Paused : KPlayerStatus 30 | object Ended : KPlayerStatus 31 | data class Error(val error: Throwable) : KPlayerStatus 32 | object Released : KPlayerStatus 33 | } 34 | 35 | typealias OnPlayerError = (error: Throwable) -> Unit 36 | typealias OnPlayerStateChanged = (KPlayerStatus) -> Unit 37 | typealias OnProgressChanged = (Long) -> Unit -------------------------------------------------------------------------------- /kvideoplayer/src/darwinMain/kotlin/com.mimao.kmp.videoplayer/KVideoPlayer.darwin.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | 3 | import kotlinx.cinterop.COpaquePointer 4 | import kotlinx.cinterop.CValue 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.Job 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.launch 11 | import platform.AVFoundation.* 12 | import platform.CoreMedia.CMTime 13 | import platform.CoreMedia.CMTimeGetSeconds 14 | import platform.CoreMedia.CMTimeMake 15 | import platform.Foundation.* 16 | import platform.darwin.NSObject 17 | import platform.darwin.dispatch_get_main_queue 18 | 19 | actual class KVideoPlayer() : IOSPlayerObserverProtocol, NSObject() { 20 | private val _status = MutableStateFlow(KPlayerStatus.Idle) 21 | actual val status: Flow 22 | get() = _status 23 | 24 | private val _volume = MutableStateFlow(1f) 25 | actual val volume: Flow 26 | get() = _volume 27 | 28 | private val _isMute = MutableStateFlow(false) 29 | actual val isMute: Flow 30 | get() = _isMute 31 | 32 | private val _currentTime = MutableStateFlow(0L) 33 | actual val currentTime: Flow 34 | get() = _currentTime 35 | 36 | private val _duration = MutableStateFlow(0L) 37 | actual val duration: Flow 38 | get() = _duration 39 | 40 | private val _isRepeated = MutableStateFlow(false) 41 | actual val isRepeated: Flow 42 | get() = _isRepeated 43 | 44 | private var player: AVPlayer? = null 45 | private val scope = CoroutineScope(Dispatchers.Main) 46 | private var repeatJob: Job? = null 47 | private var timeObserver: Any? = null 48 | private var playWhenReady = false 49 | 50 | internal var onPlayerCreated: (AVPlayer) -> Unit = {} 51 | actual fun prepare(dataSource: Any, playWhenReady: Boolean) { 52 | this.playWhenReady = playWhenReady 53 | _status.value = KPlayerStatus.Preparing 54 | val url = NSURL(string = dataSource.toString()) 55 | this.player = AVPlayer(uRL = url) 56 | 57 | player?.currentItem?.addObserver( 58 | observer = this, 59 | forKeyPath = "status", 60 | options = NSKeyValueObservingOptionNew, 61 | context = null 62 | ) 63 | 64 | timeObserver = player?.addPeriodicTimeObserverForInterval( 65 | interval = CMTimeMake(1, 1), 66 | queue = dispatch_get_main_queue(), 67 | usingBlock = { 68 | _currentTime.value = it.milliseconds() 69 | if (_currentTime.value == _duration.value) { 70 | _status.value = KPlayerStatus.Ended 71 | } 72 | } 73 | ) 74 | player?.let(onPlayerCreated) 75 | } 76 | 77 | actual fun play() { 78 | player?.play() 79 | _status.value = KPlayerStatus.Playing 80 | } 81 | 82 | actual fun pause() { 83 | player?.pause() 84 | _status.value = KPlayerStatus.Paused 85 | } 86 | 87 | actual fun stop() { 88 | player?.run { 89 | pause() 90 | seekTo(0) 91 | } 92 | repeatJob?.cancel() 93 | } 94 | 95 | actual fun release() { 96 | stop() 97 | player?.removeObserver(this, forKeyPath = "status") 98 | timeObserver?.let { 99 | player?.removeTimeObserver(it) 100 | } 101 | _status.value = KPlayerStatus.Released 102 | } 103 | 104 | actual fun seekTo(position: Long) { 105 | // TODO FIXME: seekTo is not working properly 106 | player?.seekToTime( 107 | time = CMTimeMake( 108 | value = position, 109 | timescale = 1000 110 | ) 111 | ) 112 | } 113 | 114 | actual fun setMute(mute: Boolean) { 115 | player?.volume = if (mute) 0f else _volume.value 116 | _isMute.value = mute 117 | } 118 | 119 | actual fun setVolume(volume: Float) { 120 | volume.coerceIn(0f, 1f).let { 121 | player?.volume = it 122 | _volume.value = it 123 | } 124 | } 125 | 126 | actual fun setRepeat(isRepeat: Boolean) { 127 | _isRepeated.value = isRepeat 128 | repeatJob?.cancel() 129 | if (isRepeat) { 130 | scope.launch { 131 | _status.collect { 132 | if (it == KPlayerStatus.Ended) { 133 | seekTo(0) 134 | play() 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | private fun CValue?.milliseconds(): Long { 142 | return this?.let { 143 | CMTimeGetSeconds(this).toLong() * 1000 144 | } ?: 0L 145 | } 146 | 147 | override fun observeValueForKeyPath( 148 | keyPath: String?, 149 | ofObject: Any?, 150 | change: Map?, 151 | context: COpaquePointer? 152 | ) { 153 | (change?.get("new") as NSNumber?)?.let { 154 | when (it.longValue) { 155 | AVPlayerItemStatusReadyToPlay -> { 156 | _status.value = KPlayerStatus.Ready 157 | _duration.value = player?.currentItem?.duration.milliseconds() 158 | if (playWhenReady) { 159 | play() 160 | } 161 | } 162 | 163 | AVPlayerItemStatusFailed -> { 164 | _status.value = KPlayerStatus.Error(Error("Failed to play media")) 165 | } 166 | else -> {} 167 | } 168 | } ?: println("unknown status $keyPath") 169 | } 170 | } 171 | 172 | -------------------------------------------------------------------------------- /kvideoplayer/src/desktopMain/kotlin/com/mimao/kmp/videoplayer/KVideoPlayer.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import uk.co.caprica.vlcj.media.* 6 | import uk.co.caprica.vlcj.player.base.MediaPlayer 7 | import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter 8 | import uk.co.caprica.vlcj.player.base.State 9 | import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent 10 | import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent 11 | import uk.co.caprica.vlcj.player.component.MediaPlayerComponent 12 | import java.util.Locale 13 | 14 | actual class KVideoPlayer( 15 | component: MediaPlayerComponent 16 | ) { 17 | private val player = component.mediaPlayer() 18 | 19 | private val _status = MutableStateFlow(KPlayerStatus.Idle) 20 | actual val status: Flow 21 | get() = _status 22 | 23 | private val _volume = MutableStateFlow(1f) 24 | actual val volume: Flow 25 | get() = _volume 26 | 27 | private val _isMute = MutableStateFlow(false) 28 | actual val isMute: Flow 29 | get() = _isMute 30 | 31 | private val _currentTime = MutableStateFlow(0L) 32 | actual val currentTime: Flow 33 | get() = _currentTime 34 | 35 | private val _duration = MutableStateFlow(0L) 36 | actual val duration: Flow 37 | get() = _duration 38 | 39 | private val _isRepeated = MutableStateFlow(false) 40 | actual val isRepeated: Flow 41 | get() = _isRepeated 42 | 43 | private val eventAdapter = object : MediaPlayerEventAdapter() { 44 | override fun buffering(mediaPlayer: MediaPlayer?, newCache: Float) { 45 | if (newCache == 100.0f) { 46 | _status.value = if (mediaPlayer?.status()?.isPlaying == true) KPlayerStatus.Playing else KPlayerStatus.Paused 47 | } else { 48 | _status.value = KPlayerStatus.Buffering 49 | } 50 | } 51 | 52 | override fun playing(mediaPlayer: MediaPlayer?) { 53 | _status.value = KPlayerStatus.Playing 54 | } 55 | 56 | override fun paused(mediaPlayer: MediaPlayer?) { 57 | _status.value = KPlayerStatus.Paused 58 | } 59 | 60 | /** 61 | * Waiting for this event may be more reliable than using playing(MediaPlayer) or videoOutput(MediaPlayer, int) 62 | * in some cases (logo and marquee already mentioned, also setting audio tracks, sub-title tracks and so on). 63 | */ 64 | override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) { 65 | _status.value = KPlayerStatus.Playing 66 | _duration.value = player.status().length() 67 | } 68 | 69 | override fun timeChanged(mediaPlayer: MediaPlayer?, newTime: Long) { 70 | _currentTime.value = newTime 71 | } 72 | 73 | override fun finished(mediaPlayer: MediaPlayer?) { 74 | _status.value = KPlayerStatus.Ended 75 | } 76 | 77 | override fun error(mediaPlayer: MediaPlayer?) { 78 | _status.value = KPlayerStatus.Error( 79 | Error( 80 | "Failed to load media ${ 81 | mediaPlayer?.media()?.info()?.mrl() 82 | }" 83 | ) 84 | ) 85 | } 86 | } 87 | 88 | private var currentDataSource: Any? = null 89 | actual fun prepare(dataSource: Any, playWhenReady: Boolean) { 90 | currentDataSource = dataSource 91 | _status.value = KPlayerStatus.Preparing 92 | player.events().addMediaPlayerEventListener(eventAdapter) 93 | if (playWhenReady) { 94 | player.media().play(dataSource.toString()) 95 | } else { 96 | player.media().prepare(dataSource.toString()) 97 | } 98 | _duration.value = player.status().length() 99 | _status.value = KPlayerStatus.Ready 100 | } 101 | 102 | actual fun play() { 103 | if (_status.value is KPlayerStatus.Error) { 104 | currentDataSource?.let { 105 | prepare(dataSource = it, playWhenReady = true) 106 | } 107 | } else { 108 | player.controls().play() 109 | } 110 | 111 | } 112 | 113 | actual fun pause() { 114 | player.controls().pause() 115 | } 116 | 117 | actual fun stop() { 118 | player.controls().stop() 119 | } 120 | 121 | actual fun release() { 122 | player.release() 123 | _status.value = KPlayerStatus.Released 124 | } 125 | 126 | actual fun seekTo(position: Long) { 127 | player.controls()?.setTime(position) 128 | _currentTime.value = position 129 | } 130 | 131 | actual fun setMute(mute: Boolean) { 132 | player.audio().setVolume(if (mute) 0 else _volume.value.toVLCVolume()) 133 | _isMute.value = mute 134 | } 135 | 136 | actual fun setVolume(volume: Float) { 137 | volume.coerceIn(0f, 1f).let { 138 | player.audio().setVolume(it.toVLCVolume()) 139 | _volume.value = it 140 | } 141 | } 142 | 143 | actual fun setRepeat(isRepeat: Boolean) { 144 | player.controls().repeat = isRepeat 145 | _isRepeated.value = isRepeat 146 | } 147 | 148 | private fun Float.toVLCVolume() = (this * 200).toInt() 149 | } 150 | 151 | private fun Any.mediaPlayer(): MediaPlayer { 152 | return when (this) { 153 | is CallbackMediaPlayerComponent -> mediaPlayer() 154 | is EmbeddedMediaPlayerComponent -> mediaPlayer() 155 | else -> throw IllegalArgumentException("You can only call mediaPlayer() on vlcj player component") 156 | } 157 | } 158 | 159 | fun defaultComponent(): MediaPlayerComponent = if (isMacOS()) { 160 | CallbackMediaPlayerComponent() 161 | } else { 162 | EmbeddedMediaPlayerComponent() 163 | } 164 | 165 | private fun isMacOS(): Boolean { 166 | val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) 167 | return os.indexOf("mac") >= 0 || os.indexOf("darwin") >= 0 168 | } -------------------------------------------------------------------------------- /kvideoplayer/src/iosMain/kotlin/com.mimao.kmp.videoplayer/KVideoPlayerExt.ios.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | import platform.AVKit.AVPlayerViewController 3 | 4 | fun createKVideoPlayerWithController(controller: AVPlayerViewController): KVideoPlayer { 5 | return KVideoPlayer().apply { 6 | onPlayerCreated = { 7 | controller.player = it 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /kvideoplayer/src/macosMain/kotlin/com.mimao.kmp.videoplayer/KVideoPlayerExt.macos.kt: -------------------------------------------------------------------------------- 1 | package com.mimao.kmp.videoplayer 2 | import platform.AVKit.AVPlayerView 3 | 4 | fun createKVideoPlayerWithView(view: AVPlayerView): KVideoPlayer { 5 | return KVideoPlayer().apply { 6 | onPlayerCreated = { 7 | view.player = it 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /kvideoplayer/src/nativeInterop/cinterop/observer.def: -------------------------------------------------------------------------------- 1 | language = Objective-C 2 | --- 3 | #import 4 | 5 | @protocol IOSPlayerObserver 6 | @required 7 | - (void)observeValueForKeyPath:(NSString *)keyPath 8 | ofObject:(id)object 9 | change:(NSDictionary *)change 10 | context:(void *)context; 11 | @end; -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | } 8 | } 9 | 10 | @Suppress("UnstableApiUsage") 11 | dependencyResolutionManagement { 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 16 | } 17 | } 18 | rootProject.name = "KMP-VideoPlayer" 19 | 20 | include(":kvideoplayer") 21 | include(":app:shared",":app:android", "app:desktop", "app:uikit", "app:macos" ) 22 | 23 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") --------------------------------------------------------------------------------