This commit is contained in:
Alex Kim
2024-11-17 05:48:29 +11:00
parent 3d20b7956f
commit 0b0afb448d
13 changed files with 892 additions and 265 deletions

View File

@@ -0,0 +1,70 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
group = 'expo.modules.vlcplayer'
version = '0.6.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.1.3"
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
dependencies {
implementation 'org.videolan.android:libvlc-all:4.0.0-eap15'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.31"
}
android {
namespace "expo.modules.vlcplayer"
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "0.6.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
lintOptions {
abortOnError false
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xshow-kotlin-compiler-errors"]
jvmTarget = "17"
}
}

View File

@@ -0,0 +1,2 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,69 @@
package expo.modules.vlcplayer
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class VlcPlayerModule : Module() {
override fun definition() = ModuleDefinition {
Name("VlcPlayer")
View(VlcPlayerView::class) {
Prop("source") { view: VlcPlayerView, source: Map<String, Any> ->
view.setSource(source)
}
Prop("paused") { view: VlcPlayerView, paused: Boolean ->
if (paused) {
view.pause()
} else {
view.play()
}
}
Events(
"onPlaybackStateChanged",
"onVideoStateChange",
"onVideoLoadStart",
"onVideoLoadEnd",
"onVideoProgress",
"onVideoError"
)
AsyncFunction("play") { view: VlcPlayerView ->
view.play()
}
AsyncFunction("pause") { view: VlcPlayerView ->
view.pause()
}
AsyncFunction("stop") { view: VlcPlayerView ->
view.stop()
}
AsyncFunction("seekTo") { view: VlcPlayerView, time: Int ->
view.seekTo(time)
}
// AsyncFunction("setAudioTrack") { view: VlcPlayerView, trackIndex: Int ->
// view.setAudioTrack(trackIndex)
// }
// AsyncFunction("getAudioTracks") { view: VlcPlayerView -> List<Map<String, Ansy>>? ->
// view.getAudioTracks()
// }
// AsyncFunction("setSubtitleTrack") { view: VlcPlayerView, trackIndex: Int ->
// view.setSubtitleTrack(trackIndex)
// }
// AsyncFunction("getSubtitleTracks") { view: VlcPlayerView -> List<Map<String, Any>>? ->
// view.getSubtitleTracks()
// }
// AsyncFunction("setSubtitleURL") { view: VlcPlayerView, url: String, name: String ->
// view.setSubtitleURL(url, name)
// }
}
}
}

View File

@@ -0,0 +1,251 @@
package expo.modules.vlcplayer
import android.content.Context
import android.util.Log
import android.view.ViewGroup
import android.widget.FrameLayout
import android.net.Uri
import androidx.lifecycle.LifecycleObserver
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.views.ExpoView
import org.videolan.libvlc.LibVLC
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.util.VLCVideoLayout
// Needs to inhert from MediaPlayer.EventListener
class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener {
private var libVLC: LibVLC? = null
private var mediaPlayer: MediaPlayer? = null
private lateinit var videoLayout: VLCVideoLayout
private var isPaused: Boolean = false
private var isMediaReady: Boolean = false
private var lastReportedState: Int? = null
private var lastReportedIsPlaying: Boolean? = null
private var startPosition: Int? = null
init {
setupView()
}
private fun setupView() {
setBackgroundColor(android.graphics.Color.WHITE)
videoLayout = VLCVideoLayout(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
addView(videoLayout)
}
fun setSource(source: Map<String, Any>) {
val mediaOptions = source["mediaOptions"] as? Map<String, Any> ?: emptyMap()
val initOptions = source["initOptions"] as? List<String> ?: emptyList()
val uri = source["uri"] as? String
val autoplay = source["autoplay"] as? Boolean ?: false
val isNetwork = source["isNetwork"] as? Boolean ?: false
startPosition = source["startPosition"] as? Int ?: 0
// Handle video load start event
// onVideoLoadStart?.invoke(mapOf("target" to reactTag ?: "null"))
libVLC = LibVLC(context, initOptions)
mediaPlayer = MediaPlayer(libVLC)
mediaPlayer?.attachViews(videoLayout, null, false, false)
mediaPlayer?.setEventListener(this)
Log.d("VlcPlayerView", "Loading network file: $uri")
val media = Media(libVLC, Uri.parse(uri))
mediaPlayer?.media = media
Log.d("VlcPlayerView", "Debug: Media options: $mediaOptions")
// media.addOptions(mediaOptions)
// Apply subtitle options
// val subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?: -1
// Log.d("VlcPlayerView", "Debug: Subtitle track index from source: $subtitleTrackIndex")
// if (subtitleTrackIndex >= -1) {
// setSubtitleTrack(subtitleTrackIndex)
// Log.d("VlcPlayerView", "Debug: Set subtitle track to index: $subtitleTrackIndex")
// } else {
// Log.d("VlcPlayerView", "Debug: Subtitle track index is less than -1, not setting")
// }
if (autoplay) {
Log.d("VlcPlayerView", "Playing...")
play()
// if (startPosition > 0) {
// Log.d("VlcPlayerView", "Debug: Starting at position: $startPosition")
// seekTo(startPosition)
// }
}
}
fun play() {
mediaPlayer?.play()
isPaused = false
}
fun pause() {
mediaPlayer?.pause()
isPaused = true
}
fun stop() {
mediaPlayer?.stop()
}
fun seekTo(time: Int) {
mediaPlayer?.let { player ->
val wasPlaying = player.isPlaying
if (wasPlaying) {
player.pause()
}
val duration = player.length.toInt()
val seekTime = if (time > duration) duration - 1000 else time
player.time = seekTime.toLong()
if (wasPlaying) {
player.play()
}
}
}
// fun setAudioTrack(trackIndex: Int) {
// mediaPlayer?.setAudioTrack(trackIndex)
// }
// fun getAudioTracks(): List<Map<String, Any>>? {
// val trackNames = mediaPlayer?.audioTrackNames ?: return null
// val trackIndexes = mediaPlayer?.audioTracks ?: return null
// return trackNames.zip(trackIndexes).map { (name, index) ->
// mapOf("name" to name, "index" to index)
// }
// }
// fun setSubtitleTrack(trackIndex: Int) {
// mediaPlayer?.setSpuTrack(trackIndex)
// }
// fun getSubtitleTracks(): List<Map<String, Any>>? {
// val trackNames = mediaPlayer?.spuTrackNames ?: return null
// val trackIndexes = mediaPlayer?.spuTracks ?: return null
// return trackNames.zip(trackIndexes).map { (name, index) ->
// mapOf("name" to name, "index" to index)
// }
// }
// fun setSubtitleURL(subtitleURL: String, name: String) {
// val media = mediaPlayer?.media ?: return
// media.addSlave(Media.Slave(Media.Slave.Type.Subtitle, subtitleURL, true))
// }
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mediaPlayer?.release()
libVLC?.release()
}
override fun onEvent(event: MediaPlayer.Event) {
when (event.type) {
MediaPlayer.Event.Playing,
MediaPlayer.Event.Paused,
MediaPlayer.Event.Stopped,
MediaPlayer.Event.Buffering,
MediaPlayer.Event.EndReached,
MediaPlayer.Event.EncounteredError -> updatePlayerState(event)
MediaPlayer.Event.TimeChanged -> updateVideoProgress()
}
}
private fun updatePlayerState(event: MediaPlayer.Event) {
val player = mediaPlayer ?: return
val currentState = event.type
val stateInfo = mutableMapOf<String, Any>(
"target" to "null", // Replace with actual target if needed
"currentTime" to player.time.toInt(),
"duration" to (player.media?.duration?.toInt() ?: 0),
"error" to false
)
when (currentState) {
MediaPlayer.Event.Playing -> {
stateInfo["isPlaying"] = true
stateInfo["isBuffering"] = false
stateInfo["state"] = "Playing"
}
MediaPlayer.Event.Paused -> {
stateInfo["isPlaying"] = false
stateInfo["state"] = "Paused"
}
MediaPlayer.Event.Buffering -> {
stateInfo["isBuffering"] = true
stateInfo["state"] = "Buffering"
}
MediaPlayer.Event.EncounteredError -> {
Log.e("VlcPlayerView", "player.state ~ error")
stateInfo["state"] = "Error"
onVideoLoadEnd?.invoke(stateInfo)
}
MediaPlayer.Event.Opening -> {
Log.d("VlcPlayerView", "player.state ~ opening")
stateInfo["state"] = "Opening"
}
}
// Determine if the media has finished loading
if (player.isPlaying && !isMediaReady) {
isMediaReady = true
onVideoLoadEnd?.invoke(stateInfo)
seekToStartTime()
}
if (lastReportedState != currentState || lastReportedIsPlaying != player.isPlaying) {
lastReportedState = currentState
lastReportedIsPlaying = player.isPlaying
onVideoStateChange?.invoke(stateInfo)
}
}
private fun seekToStartTime() {
val player = mediaPlayer ?: return
val startPosition = startPosition ?: return
if (startPosition > 0) {
Log.d("VlcPlayerView", "Debug: Seeking to start position: $startPosition")
player.time = startPosition.toLong()
// Ensure the player continues playing after seeking
if (!player.isPlaying) {
player.play()
}
}
}
private fun updateVideoProgress() {
val player = mediaPlayer ?: return
val currentTimeMs = player.time.toInt()
val durationMs = player.media?.duration?.toInt() ?: 0
println("currentTimeMs: $currentTimeMs")
if (currentTimeMs >= 0 && currentTimeMs < durationMs) {
onVideoProgress?.invoke(
mapOf(
"currentTime" to currentTimeMs,
"duration" to durationMs
)
)
}
}
var onVideoLoadEnd: ((Map<String, Any>) -> Unit)? = null
var onVideoStateChange: ((Map<String, Any>) -> Unit)? = null
var onVideoProgress: ((Map<String, Any>) -> Unit)? = null
}