expo-libmpv 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # expo-libmpv
2
+
3
+ A libmpv native component for Android
4
+
5
+ # Usage
6
+
7
+ I only plan on supporting Android.
8
+
9
+ iOS support contributions are welcome, but I have no way to test it.
10
+
11
+ It uses the new Fabric architecture. This replaces react-native-libmpv's Native Module.
12
+
13
+ The component will display video. Controls are handled by the app, not this library.
14
+
15
+ Take a look at https://github.com/XBigTK13X/snowstream for a real app using the library.
16
+
17
+ ## Updating the AAR
18
+
19
+ Pull down the fork of libmpv-android.
20
+
21
+ Make the needed changes.
22
+
23
+ Update the version in the kotlin file.
24
+
25
+ Run `buildscripts/docker-build.sh`
26
+
27
+ Run `buildscripts/prep-reposlite.sh VERSION`
28
+
29
+ Copy the versioned aar and pom to ~/maven-repo
30
+
31
+ Update the version in gradle.build
32
+
33
+
34
+ # Dev docs
35
+
36
+ https://docs.expo.dev/modules/native-view-tutorial/#add-an-event-to-notify-when-the-page-has-loaded
37
+
38
+ https://docs.expo.dev/modules/module-api/#events
39
+
40
+ https://docs.expo.dev/modules/module-api/#view
41
+
42
+ # Credits
43
+
44
+ I built this wrapper. But the library that drives the interactions with mpv comes from https://github.com/jarnedemeulemeester/libmpv-android.
45
+
46
+ That repo is the baseline, I merged in a PR that handle multi instance support and tweaked some things to my liking.
@@ -0,0 +1,52 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'com.libmpv'
4
+ version = '0.1.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ repositories {
21
+ google()
22
+ mavenCentral()
23
+ maven { url "/home/kretst/maven-repo" }
24
+ }
25
+ // Simple helper that allows the root project to override versions declared by this library.
26
+ ext.safeExtGet = { prop, fallback ->
27
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
28
+ }
29
+ }
30
+ project.android {
31
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
32
+ defaultConfig {
33
+ minSdkVersion 26
34
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
35
+ }
36
+ }
37
+ }
38
+
39
+ dependencies {
40
+ implementation "com.libmpv:android-libmpv:0.5.10"
41
+ }
42
+
43
+ android {
44
+ namespace "com.libmpv"
45
+ defaultConfig {
46
+ versionCode 1
47
+ versionName "0.1.0"
48
+ }
49
+ lintOptions {
50
+ abortOnError false
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,105 @@
1
+ package com.libmpv
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class LibmpvVideoModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("LibmpvVideo")
9
+ View(LibmpvVideoView::class) {
10
+ Events("onLibmpvLog", "onLibmpvEvent")
11
+
12
+ AsyncFunction("runCommand") { view: LibmpvVideoView, orders: String ->
13
+ view.runCommand(orders)
14
+ }
15
+
16
+ AsyncFunction("setOptionString") { view: LibmpvVideoView, options: String ->
17
+ view.setOptionString(options)
18
+ }
19
+
20
+ Prop("playUrl") { view: LibmpvVideoView, playUrl: String ->
21
+ view.playUrl = playUrl
22
+ if (view.isSurfaceReady()) {
23
+ view.mpv.play(playUrl)
24
+ } else {
25
+ view.attemptCreation()
26
+ }
27
+ view.log("setPlayUrl", playUrl)
28
+ }
29
+
30
+ Prop("useHardwareDecoder") { view: LibmpvVideoView, useHardwareDecoder: Boolean ->
31
+ view.useHardwareDecoder = useHardwareDecoder
32
+ if (view.isSurfaceReady()) {
33
+ view.setHardwareDecoder(useHardwareDecoder)
34
+ } else {
35
+ view.attemptCreation()
36
+ }
37
+ view.log("setUseHardwareDecoder", "$useHardwareDecoder")
38
+ }
39
+
40
+ Prop("surfaceWidth") { view: LibmpvVideoView, surfaceWidth: Int ->
41
+ view.surfaceWidth = surfaceWidth
42
+ if (view.isSurfaceReady()) {
43
+ view.mpv.setSurfaceWidth(surfaceWidth)
44
+ } else {
45
+ view.attemptCreation()
46
+ }
47
+ view.log("setSurfaceWidth", "$surfaceWidth")
48
+ }
49
+
50
+ Prop("surfaceHeight") { view: LibmpvVideoView, surfaceHeight: Int ->
51
+ view.surfaceHeight = surfaceHeight
52
+ if (view.isSurfaceReady()) {
53
+ view.mpv.setSurfaceHeight(surfaceHeight)
54
+ } else {
55
+ view.attemptCreation()
56
+ }
57
+ view.log("setSurfaceHeight", "$surfaceHeight")
58
+ }
59
+
60
+ Prop("selectedAudioTrack") { view: LibmpvVideoView, audioTrackIndex: Int ->
61
+ view.audioIndex = audioTrackIndex
62
+ if (view.isSurfaceReady()) {
63
+ val mpvIndex = if (audioTrackIndex != -1) (audioTrackIndex + 1).toString() else "no"
64
+ view.mpv.setOptionString("aid", mpvIndex)
65
+ } else {
66
+ view.attemptCreation()
67
+ }
68
+ view.log("selectAudioTrack", "$audioTrackIndex")
69
+ }
70
+
71
+ Prop("selectedSubtitleTrack") { view: LibmpvVideoView, subtitleTrackIndex: Int ->
72
+ view.subtitleIndex = subtitleTrackIndex
73
+ if (view.isSurfaceReady()) {
74
+ val mpvIndex = if (subtitleTrackIndex != -1) (subtitleTrackIndex + 1).toString() else "no"
75
+ view.mpv.setOptionString("sid", mpvIndex)
76
+ } else {
77
+ view.attemptCreation()
78
+ }
79
+ view.log("selectSubtitleTrack", "$subtitleTrackIndex")
80
+ }
81
+
82
+ Prop("seekToSeconds") { view: LibmpvVideoView, seconds: Int ->
83
+ if (view.isSurfaceReady()) {
84
+ view.mpv.seekToSeconds(seconds)
85
+ }
86
+ view.log("seekToSeconds", "$seconds")
87
+ }
88
+
89
+ Prop("isPlaying") { view: LibmpvVideoView, isPlaying: Boolean ->
90
+ if (view.isSurfaceReady() && view.mpv.hasPlayedOnce()) {
91
+ when {
92
+ isPlaying && !view.mpv.isPlaying() -> {
93
+ view.mpv.unpause()
94
+ }
95
+ !isPlaying && view.mpv.isPlaying() -> {
96
+ view.mpv.pause()
97
+ }
98
+ }
99
+ } else {
100
+ view.attemptCreation()
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,240 @@
1
+ package com.libmpv
2
+
3
+ import android.content.Context
4
+ import android.view.SurfaceHolder
5
+ import android.view.SurfaceView
6
+ import android.widget.FrameLayout
7
+ import dev.jdtech.mpv.MPVLib
8
+ import expo.modules.kotlin.AppContext
9
+ import expo.modules.kotlin.views.ExpoView
10
+ import expo.modules.kotlin.viewevent.EventDispatcher
11
+
12
+ class LibmpvVideoView(context: Context, appContext: AppContext) :
13
+ ExpoView(context,appContext),
14
+ SurfaceHolder.Callback,
15
+ MPVLib.LogObserver,
16
+ MPVLib.EventObserver {
17
+
18
+ companion object {
19
+ const val HARDWARE_OPTIONS = "mediacodec-copy"
20
+ const val ACCELERATED_CODECS = "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1"
21
+ }
22
+
23
+ private val onLibmpvLog by EventDispatcher()
24
+ private val onLibmpvEvent by EventDispatcher()
25
+
26
+ private var isSurfaceCreated: Boolean = false
27
+ val mpv: LibmpvWrapper = LibmpvWrapper(context)
28
+ private val surfaceView: SurfaceView = SurfaceView(context)
29
+
30
+ // JavaScript props
31
+ var playUrl: String? = null
32
+ var surfaceWidth: Int? = null
33
+ var surfaceHeight: Int? = null
34
+ var audioIndex: Int? = null
35
+ var subtitleIndex: Int? = null
36
+ var useHardwareDecoder: Boolean? = null
37
+
38
+ init {
39
+ surfaceView.holder.addCallback(this)
40
+ val layoutParams = FrameLayout.LayoutParams(
41
+ FrameLayout.LayoutParams.MATCH_PARENT,
42
+ FrameLayout.LayoutParams.MATCH_PARENT
43
+ )
44
+ addView(surfaceView, layoutParams)
45
+ }
46
+
47
+ fun cleanup() {
48
+ surfaceView.holder.removeCallback(this)
49
+ mpv.cleanup()
50
+ }
51
+
52
+ fun isSurfaceReady(): Boolean = isSurfaceCreated
53
+
54
+ fun attemptCreation(){
55
+ val allPropsReady = playUrl != null &&
56
+ surfaceWidth != null &&
57
+ surfaceHeight != null &&
58
+ audioIndex != null &&
59
+ subtitleIndex != null &&
60
+ useHardwareDecoder != null
61
+
62
+ if (allPropsReady) {
63
+ log("LibmpvVideoView.attemptCreation", "Initializing MPV instance")
64
+ createNativePlayer()
65
+ } else {
66
+ log("LibmpvVideoView.attemptCreation", "attemptCreation wasn't ready")
67
+ }
68
+ }
69
+
70
+ fun createNativePlayer() {
71
+ mpv.create()
72
+ prepareMpvSettings()
73
+ log("LibmpvVideoView.createNativePlayer", "mpv settings prepared. Waiting on surface creation.")
74
+ }
75
+
76
+ fun runCommand(orders: String){
77
+ mpv.command(orders.split("|").toTypedArray())
78
+ }
79
+
80
+ fun setOptionString(options: String){
81
+ val parts = options.split("|").toTypedArray()
82
+ mpv.setOptionString(parts[0],parts[1])
83
+ }
84
+
85
+ private fun prepareMpvSettings() {
86
+ mpv.addLogObserver(this)
87
+ mpv.addEventObserver(this)
88
+ mpv.setOptionString("force-window", "no")
89
+
90
+ mpv.setOptionString("config", "yes")
91
+ val mpvDir = mpv.getMpvDirectoryPath()
92
+ mpvDir?.let{
93
+ mpv.setOptionString("config-dir", mpvDir)
94
+ mpv.setOptionString("sub-font-dir", mpvDir)
95
+ }
96
+
97
+ mpv.setOptionString("keep-open", "always")
98
+ mpv.setOptionString("save-position-on-quit", "no")
99
+ mpv.setOptionString("ytdl", "no")
100
+ mpv.setOptionString("msg-level", "all=no")
101
+
102
+ mpv.setOptionString("profile", "fast")
103
+ mpv.setOptionString("vo", "gpu-next")
104
+
105
+ if (useHardwareDecoder == true) {
106
+ mpv.setOptionString("hwdec", HARDWARE_OPTIONS)
107
+ mpv.setOptionString("hwdec-codecs", ACCELERATED_CODECS)
108
+ } else {
109
+ mpv.setOptionString("hwdec", "no")
110
+ }
111
+
112
+ mpv.setOptionString("gpu-context", "android")
113
+ mpv.setOptionString("opengl-es", "yes")
114
+ mpv.setOptionString("video-sync", "audio")
115
+
116
+ mpv.setOptionString("ao", "audiotrack")
117
+ mpv.setOptionString("alang", "")
118
+
119
+ mpv.setOptionString("sub-font-provider", "none")
120
+ mpv.setOptionString("slang", "")
121
+ mpv.setOptionString("sub-scale-with-window", "yes")
122
+ mpv.setOptionString("sub-use-margins", "no")
123
+
124
+ mpv.setOptionString("cache", "yes")
125
+ mpv.setOptionString("cache-pause-initial", "yes")
126
+ mpv.setOptionString("cache-secs", "5")
127
+ mpv.setOptionString("demuxer-readahead-secs", "5")
128
+ }
129
+
130
+ fun log(method: String, argument: String) {
131
+ onLibmpvLog(mapOf(
132
+ "method" to method,
133
+ "argument" to argument
134
+ ))
135
+ }
136
+
137
+ override fun surfaceCreated(holder: SurfaceHolder) {
138
+ val width = surfaceWidth ?: 0
139
+ val height = surfaceHeight ?: 0
140
+ // In the new Fabric version, this is stretching the content
141
+ //holder.setFixedSize(width, height)
142
+ //mpv.setPropertyString("android-surface-size", "${width}x${height}")
143
+ mpv.attachSurface(surfaceView)
144
+ prepareMpvPlayback()
145
+ isSurfaceCreated = true
146
+ log("LibmpvVideoView.surfaceCreated", "Surface created and MPV should be playing")
147
+ }
148
+
149
+ private fun prepareMpvPlayback() {
150
+ mpv.init()
151
+ mpv.setOptionString("force-window", "yes")
152
+ var options = "vid=1"
153
+ options += if (audioIndex == -1) {
154
+ ",aid=no"
155
+ } else {
156
+ ",aid=${(audioIndex ?: 0) + 1}"
157
+ }
158
+ options += if (subtitleIndex == -1) {
159
+ ",sid=no"
160
+ } else {
161
+ ",sid=${(subtitleIndex ?: 0) + 1}"
162
+ }
163
+ val url: String = (playUrl as? String) ?: ""
164
+ mpv.play(url, options)
165
+ }
166
+
167
+ fun setHardwareDecoder(useHardware: Boolean) {
168
+ useHardwareDecoder = useHardware
169
+ if (useHardwareDecoder == true) {
170
+ mpv.setOptionString("hwdec", HARDWARE_OPTIONS)
171
+ mpv.setOptionString("hwdec-codecs", ACCELERATED_CODECS)
172
+ } else {
173
+ mpv.setOptionString("hwdec", "no")
174
+ }
175
+ }
176
+
177
+ override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
178
+
179
+ override fun surfaceDestroyed(holder: SurfaceHolder) {
180
+ mpv.setPropertyString("vo", "null")
181
+ mpv.setPropertyString("force-window", "no")
182
+ mpv.detachSurface()
183
+ }
184
+
185
+ // MPVLib.LogObserver
186
+ override fun logMessage(prefix: String, level: Int, text: String) {
187
+ onLibmpvLog(mapOf(
188
+ "prefix" to prefix,
189
+ "level" to "$level",
190
+ "text" to text
191
+ ))
192
+ }
193
+
194
+ // MPVLib.EventObserver
195
+ override fun eventProperty(property: String) {
196
+ onLibmpvEvent(mapOf(
197
+ "property" to property,
198
+ "kind" to "none"
199
+ ))
200
+ }
201
+
202
+ override fun eventProperty(property: String, value: Long) {
203
+ onLibmpvEvent(mapOf(
204
+ "property" to property,
205
+ "kind" to "long",
206
+ "value" to "$value"
207
+ ))
208
+ }
209
+
210
+ override fun eventProperty(property: String, value: Double) {
211
+ onLibmpvEvent(mapOf(
212
+ "property" to property,
213
+ "kind" to "double",
214
+ "value" to "$value"
215
+ ))
216
+ }
217
+
218
+ override fun eventProperty(property: String, value: Boolean) {
219
+ onLibmpvEvent(mapOf(
220
+ "property" to property,
221
+ "kind" to "boolean",
222
+ "value" to if (value) "true" else "false"
223
+ ))
224
+ }
225
+
226
+ override fun eventProperty(property: String, value: String) {
227
+ onLibmpvEvent(mapOf(
228
+ "property" to property,
229
+ "kind" to "string",
230
+ "value" to value
231
+ ))
232
+ }
233
+
234
+ override fun event(eventId: Int) {
235
+ onLibmpvEvent(mapOf(
236
+ "eventId" to "$eventId",
237
+ "kind" to "eventId"
238
+ ))
239
+ }
240
+ }