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

import android.net.Uri
import android.support.annotation.MainThread
import android.support.v4.media.session.PlaybackStateCompat
6
7
8
import android.widget.Toast
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
9
import org.videolan.libvlc.*
Geoffrey Métais's avatar
Geoffrey Métais committed
10
import org.videolan.medialibrary.media.MediaWrapper
11
import org.videolan.vlc.BuildConfig
Geoffrey Métais's avatar
Geoffrey Métais committed
12
13
14
15
16
17
18
19
20
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 {

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

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

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

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

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

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

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

    fun stop() {
        if (mediaplayer.hasMedia()) mediaplayer.stop()
Geoffrey Métais's avatar
Geoffrey Métais committed
59
        setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
60
61
62
63
64
65
66
67
    }

    fun releaseMedia() = mediaplayer.media?.let {
        it.setEventListener(null)
        it.release()
    }

    private var mediaplayerEventListener: MediaPlayer.EventListener? = null
68
    internal fun startPlayback(media: Media, listener: MediaPlayer.EventListener) {
69
        mediaplayerEventListener = listener
Geoffrey Métais's avatar
Geoffrey Métais committed
70
71
        seekable = true
        pausable = true
72
        currentTime = 0L
Geoffrey Métais's avatar
Geoffrey Métais committed
73
        length = media.duration
74
75
76
77
78
79
        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
80
81
82
83
84
85
86
87
88
89
90
        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
91
    fun seek(position: Long, length: Double = this.length.toDouble()) {
Geoffrey Métais's avatar
Geoffrey Métais committed
92
93
94
95
96
97
98
99
100
101
102
103
        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
    }

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

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

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

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

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

    fun getVideoTrack() = mediaplayer.videoTrack

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

    fun getAudioTracksCount() = mediaplayer.audioTracksCount

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

    fun getAudioTrack() = mediaplayer.audioTrack

124
125
    fun setVideoTrack(index: Int) = mediaplayer.setVideoTrack(index)

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

128
129
    fun setAudioDigitalOutputEnabled(enabled: Boolean) = mediaplayer.setAudioDigitalOutputEnabled(enabled)

Geoffrey Métais's avatar
Geoffrey Métais committed
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    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)

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

    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
154
    fun setEqualizer(equalizer: MediaPlayer.Equalizer?) = mediaplayer.setEqualizer(equalizer)
Geoffrey Métais's avatar
Geoffrey Métais committed
155
156
157
158
159
160
161
162
163
164

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

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

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

    fun release(player: MediaPlayer = mediaplayer) {
        player.setEventListener(null)
        if (isVideoPlaying()) player.vlcVout.detachViews()
173
174
175
176
177
178
179
180
181
        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
182
        setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
183
184
185
    }

    fun setSlaves(media: MediaWrapper) = launch {
186
187
        val list = MediaDatabase.getInstance().getSlaves(media.location)
        for (slave in list) mediaplayer.addSlave(slave.type, Uri.parse(slave.uri), false)
Geoffrey Métais's avatar
Geoffrey Métais committed
188
189
190
191
    }

    private fun newMediaPlayer() : MediaPlayer {
        return MediaPlayer(VLCInstance.get()).apply {
Thomas Guillem's avatar
Thomas Guillem committed
192
            setAudioDigitalOutputEnabled(VLCOptions.isAudioDigitalOutputEnabled(VLCApplication.getSettings()));
Geoffrey Métais's avatar
Geoffrey Métais committed
193
            VLCOptions.getAout(VLCApplication.getSettings())?.let { setAudioOutput(it) }
194
            setRenderer(RendererDelegate.selectedRenderer.value)
Geoffrey Métais's avatar
Geoffrey Métais committed
195
196
197
198
199
200
201
202
203
204
            this.vlcVout.addCallback(this@PlayerController)
        }
    }

    override fun onSurfacesCreated(vlcVout: IVLCVout?) {}

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

205
    fun getTime() = currentTime
Geoffrey Métais's avatar
Geoffrey Métais committed
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234

    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)

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

237
    fun getTitles(): Array<out MediaPlayer.Title>? = mediaplayer.titles
Geoffrey Métais's avatar
Geoffrey Métais committed
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254

    fun getChapterIdx() = mediaplayer.chapter

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

    fun getTitleIdx() = mediaplayer.title

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

    fun getVolume() = mediaplayer.volume

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

255
256
    suspend fun expand(): MediaList? {
        return mediaplayer.media?.let {
257
258
259
260
261
262
263
            return async(playerContext) {
                mediaplayer.setEventListener(null)
                val items = it.subItems()
                it.release()
                mediaplayer.setEventListener(this@PlayerController)
                items
            }.await()
264
265
266
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
267
268
269
270
271
    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
272
            MediaPlayer.Event.EncounteredError -> setPlaybackStopped()
Geoffrey Métais's avatar
Geoffrey Métais committed
273
274
            MediaPlayer.Event.PausableChanged -> pausable = event.pausable
            MediaPlayer.Event.SeekableChanged -> seekable = event.seekable
275
            MediaPlayer.Event.TimeChanged -> currentTime = event.timeChanged
Geoffrey Métais's avatar
Geoffrey Métais committed
276
            MediaPlayer.Event.LengthChanged -> length = event.lengthChanged
Geoffrey Métais's avatar
Geoffrey Métais committed
277
278
279
        }
        mediaplayerEventListener?.onEvent(event)
    }
280

Geoffrey Métais's avatar
Geoffrey Métais committed
281
282
283
284
285
286
    private fun setPlaybackStopped() {
        playbackState = PlaybackStateCompat.STATE_STOPPED
        currentTime = 0L
        length = 0L
    }

287
288
289
290
291
292
//    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
293
}