diff --git a/android/build.gradle b/android/build.gradle index 1ebf60c..b3cddb5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -137,6 +137,8 @@ dependencies { // Add a dependency on NitroModules implementation project(":react-native-nitro-modules") + + implementation("org.videolan.android:libvlc-all:4.0.0-eap24") } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index f5be432..5dfe049 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -1,76 +1,121 @@ package dev.zoriya.omni +import android.net.Uri +import com.margelo.nitro.NitroModules import com.margelo.nitro.omni.HybridOmniPlayerPropsSpec import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.PlayerStatus import com.margelo.nitro.omni.Rendition import com.margelo.nitro.omni.Track -import com.margelo.nitro.omni.Variant_NullType_Rendition +import dev.zoriya.omni.utils.deferredObservable +import org.videolan.libvlc.LibVLC +import org.videolan.libvlc.Media +import org.videolan.libvlc.MediaPlayer +import org.videolan.libvlc.interfaces.IMedia -class OmniPlayer(source: HybridOmniPlayerPropsSpec) : HybridOmniPlayerSpec() { - override val hasPrev: Boolean - get() = TODO("Not yet implemented") - override val hasNext: Boolean - get() = TODO("Not yet implemented") - override val status: PlayerStatus - get() = TODO("Not yet implemented") - override val isPlaying: Boolean - get() = TODO("Not yet implemented") +class OmniPlayer() : HybridOmniPlayerSpec() { + val vlc = LibVLC(NitroModules.applicationContext ?: throw Error("No Context available!")) + val player = MediaPlayer(vlc) + + var source: HybridOmniPlayerPropsSpec by deferredObservable { _, _, new -> + val src = new.src.firstOrNull()?.uri ?: return@deferredObservable + player.media = Media(vlc, Uri.parse(src)) + new.startTime?.let { start -> + player.time = (start * 1000.0).toLong().coerceAtLeast(0L) + } + } + + override val hasPrev get() = source.metadata?.hasPrev ?: false + override val hasNext get() = source.metadata?.hasNext ?: false + override val status + get() = when (player.playerState) { + IMedia.State.NothingSpecial -> PlayerStatus.IDLE + IMedia.State.Opening -> PlayerStatus.LOADING + IMedia.State.Playing -> PlayerStatus.READYTOPLAY + IMedia.State.Paused -> PlayerStatus.READYTOPLAY + IMedia.State.Stopped -> PlayerStatus.IDLE + IMedia.State.Ended -> PlayerStatus.IDLE + IMedia.State.Error -> PlayerStatus.ERROR + else -> PlayerStatus.ERROR + } + override val isPlaying: Boolean get() = player.isPlaying override var currentTime: Double - get() = TODO("Not yet implemented") - set(value) {} + get() = player.time.coerceAtLeast(0L) / 1000.0 + set(value) { + player.time = (value * 1000.0).toLong().coerceAtLeast(0L) + } override val buffered: Double - get() = TODO("Not yet implemented") - override val duration: Double - get() = TODO("Not yet implemented") + get() { + val total = duration + if (total <= 0.0) return 0.0 + return (total * player.position.toDouble()).coerceIn(0.0, total) + } + override val duration: Double get() = player.length.coerceAtLeast(0L) / 1000.0 override var playbackRate: Double - get() = TODO("Not yet implemented") - set(value) {} + get() = player.rate.toDouble() + set(value) { + if (value > 0.0) player.rate = value.toFloat() + } override var volume: Double - get() = TODO("Not yet implemented") - set(value) {} - override val videos: Array - get() = TODO("Not yet implemented") - override val audios: Array - get() = TODO("Not yet implemented") - override val subtitles: Array - get() = TODO("Not yet implemented") - override val rendition: Array - get() = TODO("Not yet implemented") + get() = (player.volume.coerceIn(0, 100) / 100.0) + set(value) { + player.volume = (value.coerceIn(0.0, 1.0) * 100.0).toInt() + } + override val videos get() = getTracks(IMedia.Track.Type.Video) + override val audios get() = getTracks(IMedia.Track.Type.Audio) + override val subtitles get() = getTracks(IMedia.Track.Type.Text) + + fun getTracks(type: Int): Array { + val tracks = player.getTracks(type) + val selected = player.getSelectedTrack(type) + return tracks.map { + Track( + id = it.id, + label = it.name, + language = it.language, + selected = it.id == selected.id + ) + }.toTypedArray() + } + + override val rendition: Array get() = emptyArray() override fun play() { - TODO("Not yet implemented") + player.play() } override fun pause() { - TODO("Not yet implemented") + player.pause() } override fun seekBy(offset: Double) { - TODO("Not yet implemented") + currentTime += offset } override fun playPrev() { - TODO("Not yet implemented") + // App-level action; no default implementation in VLC player. } override fun playNext() { - TODO("Not yet implemented") + // App-level action; no default implementation in VLC player. } override fun selectVideo(video: Track) { - TODO("Not yet implemented") + player.selectTrack(video.id) } override fun selectAudio(audio: Track) { - TODO("Not yet implemented") + player.selectTrack(audio.id) } - override fun selectSubtitle(subtitle: Track) { - TODO("Not yet implemented") + override fun selectSubtitle(subtitle: Track?) { + when (subtitle) { + null -> player.unselectTrackType(IMedia.Track.Type.Text) + else -> player.selectTrack(subtitle.id) + } } - override fun selectRendition(rendition: Variant_NullType_Rendition?) { - TODO("Not yet implemented") + override fun selectRendition(rendition: Rendition?) { + // Not supported by libVLC MediaPlayer track APIs. } -} \ No newline at end of file +} diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayerFactory.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayerFactory.kt index 622c379..f6a2891 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayerFactory.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayerFactory.kt @@ -7,6 +7,8 @@ import com.margelo.nitro.omni.HybridOmniPlayerSpec class OmniPlayerFactory(val context: ThemedReactContext): HybridOmniPlayerFactorySpec() { override fun createPlayer(props: HybridOmniPlayerPropsSpec): HybridOmniPlayerSpec { - return OmniPlayer(props) + return OmniPlayer().apply { + source = props + } } } diff --git a/android/src/main/java/dev/zoriya/omni/utils/DeferredObservable.kt b/android/src/main/java/dev/zoriya/omni/utils/DeferredObservable.kt new file mode 100644 index 0000000..20c8ed9 --- /dev/null +++ b/android/src/main/java/dev/zoriya/omni/utils/DeferredObservable.kt @@ -0,0 +1,21 @@ +package dev.zoriya.omni.utils + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +inline fun deferredObservable( + crossinline onChange: (thisRef: Any?, oldValue: T?, newValue: T) -> Unit +): ReadWriteProperty = + object : ReadWriteProperty { + private var value: T? = null + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + val old = this.value + this.value = value + onChange(old, value) + } + } diff --git a/nitrogen/generated/android/Omni+autolinking.cmake b/nitrogen/generated/android/Omni+autolinking.cmake index 392e1b8..9270301 100644 --- a/nitrogen/generated/android/Omni+autolinking.cmake +++ b/nitrogen/generated/android/Omni+autolinking.cmake @@ -41,7 +41,6 @@ target_sources( # Android-specific Nitrogen C++ sources ../nitrogen/generated/android/c++/JHybridOmniPlayerPropsSpec.cpp ../nitrogen/generated/android/c++/JHybridOmniPlayerSpec.cpp - ../nitrogen/generated/android/c++/JVariant_NullType_Rendition.cpp ../nitrogen/generated/android/c++/JHybridOmniPlayerFactorySpec.cpp ../nitrogen/generated/android/c++/JHybridOmniViewSpec.cpp ../nitrogen/generated/android/c++/views/JHybridOmniViewStateUpdater.cpp diff --git a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.cpp b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.cpp index c167e77..b3531c8 100644 --- a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.cpp @@ -23,10 +23,6 @@ namespace margelo::nitro::omni { struct Rendition; } #include #include "Rendition.hpp" #include "JRendition.hpp" -#include -#include -#include "JVariant_NullType_Rendition.hpp" -#include namespace margelo::nitro::omni { @@ -201,13 +197,13 @@ namespace margelo::nitro::omni { static const auto method = _javaPart->javaClassStatic()->getMethod /* audio */)>("selectAudio"); method(_javaPart, JTrack::fromCpp(audio)); } - void JHybridOmniPlayerSpec::selectSubtitle(const Track& subtitle) { + void JHybridOmniPlayerSpec::selectSubtitle(const std::optional& subtitle) { static const auto method = _javaPart->javaClassStatic()->getMethod /* subtitle */)>("selectSubtitle"); - method(_javaPart, JTrack::fromCpp(subtitle)); + method(_javaPart, subtitle.has_value() ? JTrack::fromCpp(subtitle.value()) : nullptr); } - void JHybridOmniPlayerSpec::selectRendition(const std::optional>& rendition) { - static const auto method = _javaPart->javaClassStatic()->getMethod /* rendition */)>("selectRendition"); - method(_javaPart, rendition.has_value() ? JVariant_NullType_Rendition::fromCpp(rendition.value()) : nullptr); + void JHybridOmniPlayerSpec::selectRendition(const std::optional& rendition) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* rendition */)>("selectRendition"); + method(_javaPart, rendition.has_value() ? JRendition::fromCpp(rendition.value()) : nullptr); } } // namespace margelo::nitro::omni diff --git a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp index 3d19653..b011212 100644 --- a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp @@ -76,8 +76,8 @@ namespace margelo::nitro::omni { void playNext() override; void selectVideo(const Track& video) override; void selectAudio(const Track& audio) override; - void selectSubtitle(const Track& subtitle) override; - void selectRendition(const std::optional>& rendition) override; + void selectSubtitle(const std::optional& subtitle) override; + void selectRendition(const std::optional& rendition) override; private: jni::global_ref _javaPart; diff --git a/nitrogen/generated/android/c++/JVariant_NullType_Rendition.cpp b/nitrogen/generated/android/c++/JVariant_NullType_Rendition.cpp deleted file mode 100644 index d8db613..0000000 --- a/nitrogen/generated/android/c++/JVariant_NullType_Rendition.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/// -/// JVariant_NullType_Rendition.cpp -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright © Marc Rousavy @ Margelo -/// - -#include "JVariant_NullType_Rendition.hpp" - -namespace margelo::nitro::omni { - /** - * Converts JVariant_NullType_Rendition to std::variant - */ - std::variant JVariant_NullType_Rendition::toCpp() const { - if (isInstanceOf(JVariant_NullType_Rendition_impl::First::javaClassStatic())) { - // It's a `nitro::NullType` - auto jniValue = static_cast(this)->getValue(); - return nitro::null; - } else if (isInstanceOf(JVariant_NullType_Rendition_impl::Second::javaClassStatic())) { - // It's a `Rendition` - auto jniValue = static_cast(this)->getValue(); - return jniValue->toCpp(); - } - throw std::invalid_argument("Variant is unknown Kotlin instance!"); - } -} // namespace margelo::nitro::omni diff --git a/nitrogen/generated/android/c++/JVariant_NullType_Rendition.hpp b/nitrogen/generated/android/c++/JVariant_NullType_Rendition.hpp deleted file mode 100644 index d89af29..0000000 --- a/nitrogen/generated/android/c++/JVariant_NullType_Rendition.hpp +++ /dev/null @@ -1,72 +0,0 @@ -/// -/// JVariant_NullType_Rendition.hpp -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright © Marc Rousavy @ Margelo -/// - -#pragma once - -#include -#include - -#include -#include "Rendition.hpp" -#include -#include -#include "JRendition.hpp" -#include - -namespace margelo::nitro::omni { - - using namespace facebook; - - /** - * The C++ JNI bridge between the C++ std::variant and the Java class "Variant_NullType_Rendition". - */ - class JVariant_NullType_Rendition: public jni::JavaClass { - public: - static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Variant_NullType_Rendition;"; - - static jni::local_ref create_0(jni::alias_ref value) { - static const auto method = javaClassStatic()->getStaticMethod)>("create"); - return method(javaClassStatic(), value); - } - static jni::local_ref create_1(jni::alias_ref value) { - static const auto method = javaClassStatic()->getStaticMethod)>("create"); - return method(javaClassStatic(), value); - } - - static jni::local_ref fromCpp(const std::variant& variant) { - switch (variant.index()) { - case 0: return create_0(JNull::null()); - case 1: return create_1(JRendition::fromCpp(std::get<1>(variant))); - default: throw std::invalid_argument("Variant holds unknown index! (" + std::to_string(variant.index()) + ")"); - } - } - - [[nodiscard]] std::variant toCpp() const; - }; - - namespace JVariant_NullType_Rendition_impl { - class First final: public jni::JavaClass { - public: - static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Variant_NullType_Rendition$First;"; - - [[nodiscard]] jni::local_ref getValue() const { - static const auto field = javaClassStatic()->getField("value"); - return getFieldValue(field); - } - }; - - class Second final: public jni::JavaClass { - public: - static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Variant_NullType_Rendition$Second;"; - - [[nodiscard]] jni::local_ref getValue() const { - static const auto field = javaClassStatic()->getField("value"); - return getFieldValue(field); - } - }; - } // namespace JVariant_NullType_Rendition_impl -} // namespace margelo::nitro::omni diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt index c67433b..6f266bf 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt @@ -10,7 +10,6 @@ package com.margelo.nitro.omni import androidx.annotation.Keep import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.NullType import com.margelo.nitro.core.HybridObject /** @@ -115,11 +114,11 @@ abstract class HybridOmniPlayerSpec: HybridObject() { @DoNotStrip @Keep - abstract fun selectSubtitle(subtitle: Track): Unit + abstract fun selectSubtitle(subtitle: Track?): Unit @DoNotStrip @Keep - abstract fun selectRendition(rendition: Variant_NullType_Rendition?): Unit + abstract fun selectRendition(rendition: Rendition?): Unit // Default implementation of `HybridObject.toString()` override fun toString(): String { diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Variant_NullType_Rendition.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Variant_NullType_Rendition.kt deleted file mode 100644 index 20344d5..0000000 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Variant_NullType_Rendition.kt +++ /dev/null @@ -1,53 +0,0 @@ -/// -/// Variant_NullType_Rendition.kt -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright © Marc Rousavy @ Margelo -/// - -package com.margelo.nitro.omni - -import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.NullType - -/** - * Represents the TypeScript variant "NullType | Rendition". - */ -@Suppress("ClassName") -@DoNotStrip -sealed class Variant_NullType_Rendition { - @DoNotStrip - data class First(@DoNotStrip val value: NullType): Variant_NullType_Rendition() - @DoNotStrip - data class Second(@DoNotStrip val value: Rendition): Variant_NullType_Rendition() - - val isFirst: Boolean - get() = this is First - val isSecond: Boolean - get() = this is Second - - fun asFirstOrNull(): NullType? { - val value = (this as? First)?.value ?: return null - return value - } - fun asSecondOrNull(): Rendition? { - val value = (this as? Second)?.value ?: return null - return value - } - - inline fun match(first: (NullType) -> R, second: (Rendition) -> R): R { - return when (this) { - is First -> first(value) - is Second -> second(value) - } - } - - companion object { - @JvmStatic - @DoNotStrip - fun create(value: NullType): Variant_NullType_Rendition = First(value) - @JvmStatic - @DoNotStrip - fun create(value: Rendition): Variant_NullType_Rendition = Second(value) - } -} diff --git a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp index ed0a0f8..a635788 100644 --- a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp @@ -24,8 +24,6 @@ namespace margelo::nitro::omni { struct Rendition; } #include "Track.hpp" #include #include "Rendition.hpp" -#include -#include #include namespace margelo::nitro::omni { @@ -81,8 +79,8 @@ namespace margelo::nitro::omni { virtual void playNext() = 0; virtual void selectVideo(const Track& video) = 0; virtual void selectAudio(const Track& audio) = 0; - virtual void selectSubtitle(const Track& subtitle) = 0; - virtual void selectRendition(const std::optional>& rendition) = 0; + virtual void selectSubtitle(const std::optional& subtitle) = 0; + virtual void selectRendition(const std::optional& rendition) = 0; protected: // Hybrid Setup diff --git a/src/types/player.ts b/src/types/player.ts index 1cead0c..d25d92e 100644 --- a/src/types/player.ts +++ b/src/types/player.ts @@ -23,9 +23,9 @@ export interface OmniPlayer { readonly audios: Track[]; selectAudio(audio: Track): void; readonly subtitles: Track[]; - selectSubtitle(subtitle: Track): void; + selectSubtitle(subtitle?: Track): void; readonly rendition: Rendition[]; - selectRendition(rendition: Rendition | null): void; + selectRendition(rendition?: Rendition): void; } export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";