PlayerController.kt 10.9 KB
Newer Older
Geoffrey Métais's avatar
Geoffrey Métais committed
1
2
package org.videolan.vlc.media

3
import android.arch.lifecycle.MutableLiveData
Geoffrey Métais's avatar
Geoffrey Métais committed
4
5
6
import android.net.Uri
import android.support.annotation.MainThread
import android.support.v4.media.session.PlaybackStateCompat
7
8
9
import android.widget.Toast
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
10
import org.videolan.libvlc.*
Geoffrey Métais's avatar
Geoffrey Métais committed
11
import org.videolan.medialibrary.media.MediaWrapper
12
import org.videolan.vlc.BuildConfig
Geoffrey Métais's avatar
Geoffrey Métais committed
13
14
15
16
17
18
19
20
21
import org.videolan.vlc.RendererDelegate
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.gui.preferences.PreferencesActivity
import org.videolan.vlc.util.VLCInstance
import org.videolan.vlc.util.VLCOptions

@Suppress("EXPERIMENTAL_FEATURE_WARNING")
class PlayerController : IVLCVout.Callback, MediaPlayer.EventListener {

22
//    private val exceptionHandler by lazy(LazyThreadSafetyMode.NONE) { CoroutineExceptionHandler { _, _ -> onPlayerError() } }
Geoffrey Métais's avatar
Geoffrey Métais committed
23
24
    private val playerContext by lazy(LazyThreadSafetyMode.NONE) { newSingleThreadContext("vlc-player") }
    private val settings by lazy(LazyThreadSafetyMode.NONE) { VLCApplication.getSettings() }
25
    val currentTime by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData<Long>() }
Geoffrey Métais's avatar
Geoffrey Métais committed
26
27
28
29
30
31
32

    private var mediaplayer = newMediaPlayer()
    var switchToVideo = false
    var seekable = false
    var pausable = false
    var previousMediaStats: Media.Stats? = null
        private set
33
    @Volatile var playbackState = PlaybackStateCompat.STATE_STOPPED
34
        private set
35
36
    @Volatile var hasRenderer = false
        private set
Geoffrey Métais's avatar
Geoffrey Métais committed
37
    @Volatile var length = 0L
38
        private set
Geoffrey Métais's avatar
Geoffrey Métais committed
39

40
    fun getVout(): IVLCVout? = mediaplayer.vlcVout
Geoffrey Métais's avatar
Geoffrey Métais committed
41

42
43
    fun canDoPassthrough() = mediaplayer.canDoPassthrough()

Geoffrey Métais's avatar
Geoffrey Métais committed
44
45
46
47
48
49
50
    fun getMedia(): Media? = mediaplayer.media

    fun play() {
        if (mediaplayer.hasMedia()) mediaplayer.play()
    }

    fun pause(): Boolean {
Geoffrey Métais's avatar
Geoffrey Métais committed
51
52
53
54
55
        if (isPlaying() && mediaplayer.hasMedia() && pausable) {
            mediaplayer.pause()
            return true
        }
        return false
Geoffrey Métais's avatar
Geoffrey Métais committed
56
57
58
59
    }

    fun stop() {
        if (mediaplayer.hasMedia()) mediaplayer.stop()
Geoffrey Métais's avatar
Geoffrey Métais committed
60
        setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
61
62
    }

63
    private fun releaseMedia() = mediaplayer.media?.let {
Geoffrey Métais's avatar
Geoffrey Métais committed
64
65
66
67
68
        it.setEventListener(null)
        it.release()
    }

    private var mediaplayerEventListener: MediaPlayer.EventListener? = null
69
    internal fun startPlayback(media: Media, listener: MediaPlayer.EventListener) {
70
        mediaplayerEventListener = listener
Geoffrey Métais's avatar
Geoffrey Métais committed
71
72
        seekable = true
        pausable = true
73
74
        currentTime.value = 0L
        lastTime = 0L
Geoffrey Métais's avatar
Geoffrey Métais committed
75
        length = media.duration
76
77
78
79
80
81
        mediaplayer.setEventListener(null)
        mediaplayer.media = media.apply { if (hasRenderer) parse() }
        mediaplayer.setEventListener(this@PlayerController)
        mediaplayer.setEqualizer(VLCOptions.getEqualizerSetFromSettings(VLCApplication.getAppContext()))
        mediaplayer.setVideoTitleDisplay(MediaPlayer.Position.Disable, 0)
        mediaplayer.play()
Geoffrey Métais's avatar
Geoffrey Métais committed
82
83
84
85
86
87
88
89
90
91
92
        if (mediaplayer.rate == 1.0f && settings.getBoolean(PreferencesActivity.KEY_PLAYBACK_SPEED_PERSIST, true))
            setRate(settings.getFloat(PreferencesActivity.KEY_PLAYBACK_RATE, 1.0f), false)
    }

    @MainThread
    fun restart() {
        val mp = mediaplayer
        mediaplayer = newMediaPlayer()
        release(mp)
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
93
    fun seek(position: Long, length: Double = this.length.toDouble()) {
Geoffrey Métais's avatar
Geoffrey Métais committed
94
95
96
97
98
99
100
101
102
103
104
105
        if (length > 0.0) setPosition((position / length).toFloat())
        else setTime(position)
    }

    fun setPosition(position: Float) {
        if (seekable) mediaplayer.position = position
    }

    fun setTime(time: Long) {
        if (seekable) mediaplayer.time = time
    }

106
    fun isPlaying() = playbackState == PlaybackStateCompat.STATE_PLAYING
Geoffrey Métais's avatar
Geoffrey Métais committed
107
108
109
110
111
112
113

    fun isVideoPlaying() = mediaplayer.vlcVout.areViewsAttached()

    fun canSwitchToVideo() = mediaplayer.hasMedia() && mediaplayer.videoTracksCount > 0

    fun getVideoTracksCount() = if (mediaplayer.hasMedia()) mediaplayer.videoTracksCount else 0

114
    fun getVideoTracks(): Array<out MediaPlayer.TrackDescription>? = mediaplayer.videoTracks
Geoffrey Métais's avatar
Geoffrey Métais committed
115
116
117

    fun getVideoTrack() = mediaplayer.videoTrack

118
    fun getCurrentVideoTrack(): Media.VideoTrack? = mediaplayer.currentVideoTrack
Geoffrey Métais's avatar
Geoffrey Métais committed
119
120
121

    fun getAudioTracksCount() = mediaplayer.audioTracksCount

122
    fun getAudioTracks(): Array<out MediaPlayer.TrackDescription>? = mediaplayer.audioTracks
Geoffrey Métais's avatar
Geoffrey Métais committed
123
124
125

    fun getAudioTrack() = mediaplayer.audioTrack

126
127
    fun setVideoTrack(index: Int) = mediaplayer.setVideoTrack(index)

Geoffrey Métais's avatar
Geoffrey Métais committed
128
129
    fun setAudioTrack(index: Int) = mediaplayer.setAudioTrack(index)

130
131
    fun setAudioDigitalOutputEnabled(enabled: Boolean) = mediaplayer.setAudioDigitalOutputEnabled(enabled)

Geoffrey Métais's avatar
Geoffrey Métais committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    fun getAudioDelay() = mediaplayer.audioDelay

    fun getSpuDelay() = mediaplayer.spuDelay

    fun getRate() = mediaplayer.rate

    fun setSpuDelay(delay: Long) = mediaplayer.setSpuDelay(delay)

    fun setVideoTrackEnabled(enabled: Boolean) = mediaplayer.setVideoTrackEnabled(enabled)

    fun addSubtitleTrack(path: String, select: Boolean) = mediaplayer.addSlave(Media.Slave.Type.Subtitle, path, select)

    fun addSubtitleTrack(uri: Uri, select: Boolean) = mediaplayer.addSlave(Media.Slave.Type.Subtitle, uri, select)

146
    fun getSpuTracks(): Array<out MediaPlayer.TrackDescription>? = mediaplayer.spuTracks
Geoffrey Métais's avatar
Geoffrey Métais committed
147
148
149
150
151
152
153
154
155

    fun getSpuTrack() = mediaplayer.spuTrack

    fun setSpuTrack(index: Int) = mediaplayer.setSpuTrack(index)

    fun getSpuTracksCount() = mediaplayer.spuTracksCount

    fun setAudioDelay(delay: Long) = mediaplayer.setAudioDelay(delay)

Geoffrey Métais's avatar
Geoffrey Métais committed
156
    fun setEqualizer(equalizer: MediaPlayer.Equalizer?) = mediaplayer.setEqualizer(equalizer)
Geoffrey Métais's avatar
Geoffrey Métais committed
157
158
159
160
161
162
163
164
165
166

    @MainThread
    fun setVideoScale(scale: Float) {
        mediaplayer.scale = scale
    }

    fun setVideoAspectRatio(aspect: String?) {
        mediaplayer.aspectRatio = aspect
    }

167
168
169
170
    fun setRenderer(renderer: RendererItem?) {
        mediaplayer.setRenderer(renderer)
        hasRenderer = renderer !== null
    }
Geoffrey Métais's avatar
Geoffrey Métais committed
171
172
173
174

    fun release(player: MediaPlayer = mediaplayer) {
        player.setEventListener(null)
        if (isVideoPlaying()) player.vlcVout.detachViews()
175
        releaseMedia()
176
177
178
179
180
181
182
183
184
        launch(newSingleThreadContext("vlc-player-release")) {
            if (BuildConfig.DEBUG) { // Warn if player release is blocking
                try {
                    withTimeout(5000, { player.release() })
                } catch (exception: TimeoutCancellationException) {
                    launch(UI) { Toast.makeText(VLCApplication.getAppContext(), "media stop has timeouted!", Toast.LENGTH_LONG).show() }
                }
            } else player.release()
        }
Geoffrey Métais's avatar
Geoffrey Métais committed
185
        setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
186
187
    }

188
189
190
191
192
193
194
195
    suspend fun setSlaves(media: Media, mw: MediaWrapper) {
        val list = withContext(CommonPool) {
            mw.slaves?.let {
                for (slave in it) media.addSlave(slave)
                MediaDatabase.getInstance().saveSlaves(mw)
            }
            MediaDatabase.getInstance().getSlaves(mw.location)
        }
196
        for (slave in list) mediaplayer.addSlave(slave.type, Uri.parse(slave.uri), false)
Geoffrey Métais's avatar
Geoffrey Métais committed
197
198
199
200
    }

    private fun newMediaPlayer() : MediaPlayer {
        return MediaPlayer(VLCInstance.get()).apply {
Thomas Guillem's avatar
Thomas Guillem committed
201
            setAudioDigitalOutputEnabled(VLCOptions.isAudioDigitalOutputEnabled(VLCApplication.getSettings()));
Geoffrey Métais's avatar
Geoffrey Métais committed
202
            VLCOptions.getAout(VLCApplication.getSettings())?.let { setAudioOutput(it) }
203
            setRenderer(RendererDelegate.selectedRenderer.value)
Geoffrey Métais's avatar
Geoffrey Métais committed
204
205
206
207
208
209
210
211
212
213
            this.vlcVout.addCallback(this@PlayerController)
        }
    }

    override fun onSurfacesCreated(vlcVout: IVLCVout?) {}

    override fun onSurfacesDestroyed(vlcVout: IVLCVout?) {
        switchToVideo = false
    }

214
    fun getCurrentTime() = currentTime.value ?: 0L
Geoffrey Métais's avatar
Geoffrey Métais committed
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

    fun setRate(rate: Float, save: Boolean) {
        mediaplayer.rate = rate
        if (save && settings.getBoolean(PreferencesActivity.KEY_PLAYBACK_SPEED_PERSIST, true))
            settings.edit().putFloat(PreferencesActivity.KEY_PLAYBACK_RATE, rate).apply()
    }

    /**
     * Update current media meta and return true if player needs to be updated
     *
     * @param id of the Meta event received, -1 for none
     * @return true if UI needs to be updated
     */
    internal fun updateCurrentMeta(id: Int, mw: MediaWrapper?): Boolean {
        if (id == Media.Meta.Publisher) return false
        mw?.updateMeta(mediaplayer)
        return id != Media.Meta.NowPlaying || mw?.nowPlaying !== null
    }

    fun setPreviousStats() {
        val media = mediaplayer.media ?: return
        previousMediaStats = media.stats
        media.release()
    }

    fun updateViewpoint(yaw: Float, pitch: Float, roll: Float, fov: Float, absolute: Boolean) = mediaplayer.updateViewpoint(yaw, pitch, roll, fov, absolute)

    fun navigate(where: Int) = mediaplayer.navigate(where)

244
    fun getChapters(title: Int): Array<out MediaPlayer.Chapter>? = mediaplayer.getChapters(title)
Geoffrey Métais's avatar
Geoffrey Métais committed
245

246
    fun getTitles(): Array<out MediaPlayer.Title>? = mediaplayer.titles
Geoffrey Métais's avatar
Geoffrey Métais committed
247
248
249
250
251
252
253
254
255
256
257
258
259

    fun getChapterIdx() = mediaplayer.chapter

    fun setChapterIdx(chapter: Int) {
        mediaplayer.chapter = chapter
    }

    fun getTitleIdx() = mediaplayer.title

    fun setTitleIdx(title: Int) {
        mediaplayer.title = title
    }

260
    fun getVolume() = if (mediaplayer.hasMedia()) mediaplayer.volume else 100
Geoffrey Métais's avatar
Geoffrey Métais committed
261
262
263

    fun setVolume(volume: Int) = mediaplayer.setVolume(volume)

264
265
    suspend fun expand(): MediaList? {
        return mediaplayer.media?.let {
266
267
268
269
270
271
272
            return async(playerContext) {
                mediaplayer.setEventListener(null)
                val items = it.subItems()
                it.release()
                mediaplayer.setEventListener(this@PlayerController)
                items
            }.await()
273
274
275
        }
    }

276
    private var lastTime = 0L
Geoffrey Métais's avatar
Geoffrey Métais committed
277
278
279
280
281
    override fun onEvent(event: MediaPlayer.Event?) {
        if (event === null) return
        when(event.type) {
            MediaPlayer.Event.Playing -> playbackState = PlaybackStateCompat.STATE_PLAYING
            MediaPlayer.Event.Paused -> playbackState = PlaybackStateCompat.STATE_PAUSED
Geoffrey Métais's avatar
Geoffrey Métais committed
282
            MediaPlayer.Event.EncounteredError -> setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
283
284
            MediaPlayer.Event.PausableChanged -> pausable = event.pausable
            MediaPlayer.Event.SeekableChanged -> seekable = event.seekable
Geoffrey Métais's avatar
Geoffrey Métais committed
285
            MediaPlayer.Event.LengthChanged -> length = event.lengthChanged
286
287
288
289
290
291
292
            MediaPlayer.Event.TimeChanged -> {
                val time = event.timeChanged
                if (time - lastTime > 950L) {
                    currentTime.value = time
                    lastTime = time
                }
            }
Geoffrey Métais's avatar
Geoffrey Métais committed
293
294
295
        }
        mediaplayerEventListener?.onEvent(event)
    }
296

Geoffrey Métais's avatar
Geoffrey Métais committed
297
298
    private fun setPlaybackStopped() {
        playbackState = PlaybackStateCompat.STATE_STOPPED
299
300
        currentTime.value = 0L
        lastTime = 0L
Geoffrey Métais's avatar
Geoffrey Métais committed
301
302
303
        length = 0L
    }

304
305
306
307
308
309
//    private fun onPlayerError() {
//        launch(UI) {
//            restart()
//            Toast.makeText(VLCApplication.getAppContext(), VLCApplication.getAppContext().getString(R.string.feedback_player_crashed), Toast.LENGTH_LONG).show()
//        }
//    }
Geoffrey Métais's avatar
Geoffrey Métais committed
310
}