expo-libmpv 0.4.6 → 0.5.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.
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'com.libmpv'
4
- version = '0.4.5'
4
+ version = '0.5.0'
5
5
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
6
6
  apply from: expoModulesCorePlugin
7
7
  applyKotlinExpoModulesCorePlugin()
@@ -36,14 +36,14 @@ if (useManagedAndroidSdkVersions) {
36
36
  }
37
37
 
38
38
  dependencies {
39
- implementation "com.libmpv:android-libmpv:0.7.1"
39
+ implementation "com.libmpv:android-libmpv:0.7.3"
40
40
  }
41
41
 
42
42
  android {
43
43
  namespace "com.libmpv"
44
44
  defaultConfig {
45
45
  versionCode 1
46
- versionName "0.4.5"
46
+ versionName "0.5.0"
47
47
  }
48
48
  lintOptions {
49
49
  abortOnError false
@@ -0,0 +1,400 @@
1
+ package com.libmpv
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.util.Log
6
+ import android.view.SurfaceView
7
+ import com.libmpv.LibmpvSession
8
+ import dev.jdtech.mpv.MPVLib
9
+ import java.io.File
10
+ import java.io.FileOutputStream
11
+
12
+ class LibmpvRenderer(
13
+ private val session: LibmpvSession,
14
+ private val surfaceView: SurfaceView,
15
+ private val onLog: (Map<String, Any>) -> Unit,
16
+ private val onEvent: (Map<String, Any>) -> Unit
17
+ ) : MPVLib.EventObserver, MPVLib.LogObserver {
18
+
19
+ private enum class State {
20
+ NEW,
21
+ CREATED,
22
+ INITIALIZED,
23
+ ACTIVE,
24
+ SHUTTING_DOWN,
25
+ DESTROYED
26
+ }
27
+
28
+ companion object {
29
+ private const val TAG = "expo-libmpv"
30
+ private const val LOG_LEVEL_WARN = 30
31
+ }
32
+
33
+ private val stateLock = Any()
34
+
35
+ @Volatile private var state: State = State.NEW
36
+ @Volatile private var destroyed = false
37
+ @Volatile private var loadedUrl: String? = null
38
+
39
+ private var mpvDirectory: String? = null
40
+
41
+ private inline val mpvAlive: Boolean
42
+ get() = when (state) {
43
+ State.CREATED, State.INITIALIZED, State.ACTIVE -> true
44
+ else -> false
45
+ }
46
+
47
+ private inline val shuttingDown: Boolean
48
+ get() = state == State.SHUTTING_DOWN || state == State.DESTROYED
49
+
50
+ private val mainHandler = Handler(Looper.getMainLooper())
51
+
52
+ fun runCommand(command: Array<String>) {
53
+ if (!mpvAlive || shuttingDown) return
54
+ try {
55
+ MPVLib.command(command)
56
+ } catch (e: Exception) {
57
+ logException(e)
58
+ }
59
+ }
60
+
61
+ fun setOptionString(option: String, value: String) {
62
+ if (!mpvAlive || shuttingDown) return
63
+ try {
64
+ MPVLib.setOptionString(option, value)
65
+ } catch (e: Exception) {
66
+ logException(e)
67
+ }
68
+ }
69
+
70
+
71
+ fun start() {
72
+ synchronized(stateLock) {
73
+ if (state != State.NEW && state != State.DESTROYED) return
74
+ try {
75
+ MPVLib.create(surfaceView.context.applicationContext)
76
+ createMpvDirectory()
77
+ state = State.CREATED
78
+ } catch (e: Exception) {
79
+ logException(e)
80
+ return
81
+ }
82
+ }
83
+
84
+ initMpv()
85
+ attachSurfaceAndConfigure()
86
+ }
87
+
88
+ fun destroy() {
89
+ val doTeardown: Boolean
90
+ synchronized(stateLock) {
91
+ doTeardown = when (state) {
92
+ State.NEW, State.SHUTTING_DOWN, State.DESTROYED -> false
93
+ else -> true
94
+ }
95
+ if (doTeardown) state = State.SHUTTING_DOWN
96
+ }
97
+ if (!doTeardown) return
98
+
99
+ destroyed = true
100
+
101
+ try {
102
+ loadedUrl = null
103
+ stopPlayback()
104
+ detachSurfaceInternal()
105
+
106
+ mainHandler.post {
107
+ try {
108
+ MPVLib.destroy()
109
+ } finally {
110
+ synchronized(stateLock) {
111
+ state = State.DESTROYED
112
+ }
113
+ }
114
+ }
115
+ } catch (e: Exception) {
116
+ logException(e)
117
+ synchronized(stateLock) {
118
+ state = State.DESTROYED
119
+ }
120
+ }
121
+ }
122
+
123
+
124
+ fun onSessionUpdated() {
125
+ if (!mpvAlive || shuttingDown) return
126
+ maybeStartPlayback()
127
+ applyDeferredState()
128
+ applyContinuousState()
129
+ }
130
+
131
+ private fun initMpv() {
132
+ synchronized(stateLock) {
133
+ if (shuttingDown || state != State.CREATED) return
134
+ try {
135
+ MPVLib.init()
136
+ state = State.INITIALIZED
137
+ } catch (e: Exception) {
138
+ logException(e)
139
+ return
140
+ }
141
+ }
142
+ addObservers()
143
+ }
144
+
145
+ private fun attachSurfaceAndConfigure() {
146
+ if (!mpvAlive || shuttingDown) return
147
+ try {
148
+ MPVLib.attachSurface(surfaceView.holder.surface)
149
+ synchronized(stateLock) {
150
+ if (state == State.CREATED || state == State.INITIALIZED) {
151
+ state = State.ACTIVE
152
+ }
153
+ }
154
+ applyConfiguration()
155
+ MPVLib.setPropertyString("pause", "yes")
156
+ maybeStartPlayback()
157
+ } catch (e: Exception) {
158
+ logException(e)
159
+ }
160
+ }
161
+
162
+ private fun addObservers() {
163
+ if (!mpvAlive || shuttingDown) return
164
+ try {
165
+ MPVLib.removeObservers()
166
+ MPVLib.addObserver(this)
167
+ MPVLib.removeLogObservers()
168
+ MPVLib.addLogObserver(this)
169
+
170
+ MPVLib.observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
171
+ MPVLib.observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
172
+ MPVLib.observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
173
+ MPVLib.observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
174
+ MPVLib.observeProperty("seekable", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
175
+ MPVLib.observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
176
+ MPVLib.observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
177
+ MPVLib.observeProperty("track-list", MPVLib.MpvFormat.MPV_FORMAT_STRING)
178
+ } catch (e: Exception) {
179
+ logException(e)
180
+ }
181
+ }
182
+
183
+ private fun applyConfiguration() {
184
+ if (!mpvAlive || shuttingDown) return
185
+ try {
186
+ MPVLib.setOptionString("force-window", "no")
187
+ MPVLib.setOptionString("config", "yes")
188
+ mpvDirectory?.let {
189
+ MPVLib.setOptionString("config-dir", it)
190
+ MPVLib.setOptionString("sub-font-dir", it)
191
+ }
192
+
193
+ MPVLib.setOptionString("keep-open", "always")
194
+ MPVLib.setOptionString("save-position-on-quit", "no")
195
+ MPVLib.setOptionString("ytdl", "no")
196
+ MPVLib.setOptionString("msg-level", "all=no")
197
+
198
+ session.videoOutput?.let { videoOutput ->
199
+ MPVLib.setOptionString("vo", videoOutput)
200
+ }
201
+
202
+ val decodingMode = session.decodingMode
203
+ val acceleratedCodecs = session.acceleratedCodecs
204
+ if (!decodingMode.isNullOrBlank() && !acceleratedCodecs.isNullOrBlank()) {
205
+ MPVLib.setOptionString("hwdec", decodingMode)
206
+ if (decodingMode != "no"){
207
+ MPVLib.setOptionString("hwdec-codecs", acceleratedCodecs)
208
+ }
209
+ }
210
+
211
+ MPVLib.setOptionString("gpu-context", "android")
212
+ MPVLib.setOptionString("opengl-es", "yes")
213
+
214
+ val videoSync = session.videoSync
215
+ if(!videoSync.isNullOrBlank()){
216
+ if(videoSync == "display-resample"){
217
+ MPVLib.setOptionString("video-sync", "display-resample")
218
+ MPVLib.setOptionString("audio-pitch-correction","no")
219
+ MPVLib.setOptionString("autosync", "1")
220
+ MPVLib.setOptionString("correct-pts","no")
221
+ }
222
+ if(videoSync == "audio"){
223
+ MPVLib.setOptionString("video-sync", "audio")
224
+ MPVLib.setOptionString("audio-pitch-correction","yes")
225
+ MPVLib.setOptionString("autosync", "0")
226
+ MPVLib.setOptionString("correct-pts","yes")
227
+ }
228
+ }
229
+
230
+ MPVLib.setOptionString("scale", "bilinear")
231
+ MPVLib.setOptionString("dscale", "bilinear")
232
+ MPVLib.setOptionString("tscale","off")
233
+ MPVLib.setOptionString("interpolation","no")
234
+
235
+ MPVLib.setOptionString("ao", "audiotrack")
236
+ MPVLib.setOptionString("alang", "")
237
+
238
+ MPVLib.setOptionString("sub-font-provider", "none")
239
+ MPVLib.setOptionString("slang", "")
240
+ MPVLib.setOptionString("sub-scale-with-window", "yes")
241
+ MPVLib.setOptionString("sub-use-margins", "no")
242
+
243
+ MPVLib.setOptionString("cache", "yes")
244
+ MPVLib.setOptionString("cache-pause-initial", "yes")
245
+ MPVLib.setOptionString("audio-buffer","2.0")
246
+
247
+ } catch (e: Exception) {
248
+ logException(e)
249
+ }
250
+ }
251
+
252
+ private fun maybeStartPlayback() {
253
+ val url = session.playUrl ?: return
254
+ if (loadedUrl == url) return
255
+ try {
256
+ loadedUrl = url
257
+ MPVLib.command(arrayOf("loadfile", url, "replace"))
258
+ applyContinuousState()
259
+ } catch (e: Exception) {
260
+ logException(e)
261
+ }
262
+ }
263
+
264
+ private fun applyContinuousState() {
265
+ if (!mpvAlive || shuttingDown) return
266
+ if (!session.hasFileLoaded) return
267
+
268
+ MPVLib.command(
269
+ arrayOf("set", "pause", if (session.isPlaying) "no" else "yes")
270
+ )
271
+ }
272
+
273
+ private fun applyDeferredState() {
274
+ if (!mpvAlive || shuttingDown) return
275
+
276
+ session.seekToSeconds?.let { target ->
277
+ if (session.needsApply(LibmpvSession.MpvIntent.SEEK)) {
278
+ val timePos = MPVLib.getPropertyDouble("time-pos")
279
+ val seekable = MPVLib.getPropertyBoolean("seekable") == true
280
+
281
+ if (seekable && timePos != null && kotlin.math.abs(timePos - target) > 0.5) {
282
+ MPVLib.command(arrayOf("seek", target.toString(), "absolute"))
283
+ session.markApplied(LibmpvSession.MpvIntent.SEEK)
284
+ }
285
+ }
286
+ }
287
+
288
+ session.selectedAudioTrack?.let {
289
+ if (session.needsApply(LibmpvSession.MpvIntent.AUDIO_TRACK)) {
290
+ val aid = if (it == -1) "no" else (it + 1).toString()
291
+ MPVLib.command(arrayOf("set", "aid", aid))
292
+ session.markApplied(LibmpvSession.MpvIntent.AUDIO_TRACK)
293
+ }
294
+ }
295
+
296
+ session.selectedSubtitleTrack?.let {
297
+ if (session.needsApply(LibmpvSession.MpvIntent.SUBTITLE_TRACK)) {
298
+ val sid = if (it == -1) "no" else (it + 1).toString()
299
+ MPVLib.command(arrayOf("set", "sid", sid))
300
+ session.markApplied(LibmpvSession.MpvIntent.SUBTITLE_TRACK)
301
+ }
302
+ }
303
+ }
304
+
305
+ private fun stopPlayback() {
306
+ if (!mpvAlive) return
307
+ try {
308
+ MPVLib.command(arrayOf("stop"))
309
+ MPVLib.setPropertyString("pause", "yes")
310
+ MPVLib.setPropertyString("vo", "null")
311
+ MPVLib.setPropertyString("ao", "null")
312
+ } catch (e: Exception) {
313
+ logException(e)
314
+ }
315
+ }
316
+
317
+ private fun detachSurfaceInternal() {
318
+ try {
319
+ MPVLib.detachSurface()
320
+ } catch (e: Exception) {
321
+ logException(e)
322
+ }
323
+ }
324
+
325
+ override fun logMessage(prefix: String, level: Int, text: String) {
326
+ if (shuttingDown) return
327
+ if (level <= LOG_LEVEL_WARN) Log.w(TAG, "[$prefix] $text")
328
+ onLog(mapOf("prefix" to prefix, "level" to level, "text" to text))
329
+ }
330
+
331
+ override fun event(eventId: Int) {
332
+ when (eventId) {
333
+ MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED,
334
+ MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> {
335
+ session.hasFileLoaded = true
336
+ mainHandler.post {
337
+ applyDeferredState()
338
+ MPVLib.setPropertyString("pause", if (session.isPlaying) "no" else "yes")
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+
345
+ override fun eventProperty(property: String) =
346
+ onEvent(mapOf("property" to property, "kind" to "none"))
347
+
348
+ override fun eventProperty(property: String, value: Long) =
349
+ onEvent(mapOf("property" to property, "kind" to "long", "value" to value))
350
+
351
+ override fun eventProperty(property: String, value: Double) =
352
+ onEvent(mapOf("property" to property, "kind" to "double", "value" to value))
353
+
354
+ override fun eventProperty(property: String, value: Boolean) =
355
+ onEvent(mapOf("property" to property, "kind" to "boolean", "value" to value))
356
+
357
+ override fun eventProperty(property: String, value: String) =
358
+ onEvent(mapOf("property" to property, "kind" to "string", "value" to value))
359
+
360
+ private fun createMpvDirectory() {
361
+ if (shuttingDown) return
362
+
363
+ val ctx = surfaceView.context.applicationContext
364
+ val dir = File(ctx.getExternalFilesDir("mpv"), "mpv")
365
+
366
+ try {
367
+ if (!dir.exists() && !dir.mkdirs()) return
368
+ mpvDirectory = dir.absolutePath
369
+
370
+ val subFont = File(dir, "subfont.ttf")
371
+ if (!subFont.exists()) {
372
+ ctx.assets.open("subfont.ttf").use { inS ->
373
+ FileOutputStream(subFont).use { outS ->
374
+ inS.copyTo(outS)
375
+ }
376
+ }
377
+ }
378
+
379
+ val mpvConf = File(dir, "mpv.conf")
380
+ if (!mpvConf.exists()) {
381
+ ctx.assets.open("mpv.conf").use { inS ->
382
+ FileOutputStream(mpvConf).use { outS ->
383
+ inS.copyTo(outS)
384
+ }
385
+ }
386
+ }
387
+ } catch (e: Exception) {
388
+ Log.e(TAG, "mpv directory init failed", e)
389
+ }
390
+ }
391
+
392
+ private fun logException(e: Exception) {
393
+ if (shuttingDown) return
394
+ try {
395
+ MPVLib.logMessage("RNLE", 20, e.message ?: "Unknown mpv error")
396
+ } catch (_: Exception) {
397
+ Log.e(TAG, "mpv error", e)
398
+ }
399
+ }
400
+ }
@@ -0,0 +1,52 @@
1
+ package com.libmpv
2
+
3
+ import java.util.EnumSet
4
+
5
+ class LibmpvSession {
6
+ var playUrl: String? = null
7
+ set(value) {
8
+ if (field != value) {
9
+ field = value
10
+ resetForNewMedia()
11
+ }
12
+ }
13
+ var hasFileLoaded: Boolean = false
14
+
15
+ var seekToSeconds: Double? = null
16
+ var selectedAudioTrack: Int? = null
17
+ var selectedSubtitleTrack: Int? = null
18
+
19
+ var isPlaying: Boolean = true
20
+
21
+ var videoOutput: String? = null
22
+ var decodingMode: String? = null
23
+ var acceleratedCodecs: String? = null
24
+ var videoSync: String? = null
25
+
26
+ var surfaceWidth: Int? = null
27
+ var surfaceHeight: Int? = null
28
+
29
+ enum class MpvIntent {
30
+ SEEK,
31
+ AUDIO_TRACK,
32
+ SUBTITLE_TRACK
33
+ }
34
+
35
+ private val applied = EnumSet.noneOf(MpvIntent::class.java)
36
+
37
+ fun needsApply(intent: MpvIntent): Boolean =
38
+ !applied.contains(intent)
39
+
40
+ fun markApplied(intent: MpvIntent) {
41
+ applied.add(intent)
42
+ }
43
+
44
+ fun markDirty(intent: MpvIntent) {
45
+ applied.remove(intent)
46
+ }
47
+
48
+ fun resetForNewMedia() {
49
+ hasFileLoaded = false
50
+ applied.clear()
51
+ }
52
+ }