@@ -18,105 +18,194 @@ package im.vector.app.features.voicebroadcast
18
18
19
19
import android.media.AudioAttributes
20
20
import android.media.MediaPlayer
21
+ import im.vector.app.core.di.ActiveSessionHolder
21
22
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
22
- import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
23
23
import im.vector.app.features.voice.VoiceFailure
24
+ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
25
+ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
26
+ import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
24
27
import kotlinx.coroutines.CoroutineScope
25
28
import kotlinx.coroutines.Dispatchers
29
+ import kotlinx.coroutines.Job
30
+ import kotlinx.coroutines.SupervisorJob
26
31
import kotlinx.coroutines.launch
27
- import org.matrix.android.sdk.api.extensions.orFalse
28
- import org.matrix.android.sdk.api.session.Session
29
32
import org.matrix.android.sdk.api.session.events.model.RelationType
30
33
import org.matrix.android.sdk.api.session.events.model.getRelationContent
31
34
import org.matrix.android.sdk.api.session.getRoom
32
35
import org.matrix.android.sdk.api.session.room.Room
33
36
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
34
37
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
35
38
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
39
+ import org.matrix.android.sdk.api.session.room.timeline.Timeline
40
+ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
41
+ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
36
42
import timber.log.Timber
37
43
import javax.inject.Inject
38
44
import javax.inject.Singleton
39
45
40
46
@Singleton
41
47
class VoiceBroadcastPlayer @Inject constructor(
42
- private val session : Session ,
48
+ private val sessionHolder : ActiveSessionHolder ,
43
49
private val playbackTracker : AudioMessagePlaybackTracker ,
50
+ private val getVoiceBroadcastUseCase : GetVoiceBroadcastUseCase ,
44
51
) {
52
+ private val session
53
+ get() = sessionHolder.getActiveSession()
45
54
46
- private val mediaPlayerScope = CoroutineScope (Dispatchers .IO )
55
+ private val coroutineScope = CoroutineScope (SupervisorJob () + Dispatchers .Default )
56
+ private var voiceBroadcastStateJob: Job ? = null
57
+ private var currentTimeline: Timeline ? = null
58
+ set(value) {
59
+ field?.removeAllListeners()
60
+ field?.dispose()
61
+ field = value
62
+ }
63
+
64
+ private val mediaPlayerListener = MediaPlayerListener ()
65
+ private var timelineListener: TimelineListener ? = null
47
66
48
67
private var currentMediaPlayer: MediaPlayer ? = null
49
- private var currentPlayingIndex: Int = - 1
68
+ private var nextMediaPlayer: MediaPlayer ? = null
69
+ set(value) {
70
+ field = value
71
+ currentMediaPlayer?.setNextMediaPlayer(value)
72
+ }
73
+ private var currentSequence: Int? = null
74
+
50
75
private var playlist = emptyList<MessageAudioEvent >()
51
- private val currentVoiceBroadcastEventId
76
+ private val currentVoiceBroadcastId
52
77
get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId
53
78
54
- private val mediaPlayerListener = MediaPlayerListener ()
55
-
56
- fun play (roomId : String , eventId : String ) {
57
- val room = session.getRoom(roomId) ? : error(" Unknown roomId: $roomId " )
79
+ private var state: State = State .IDLE
80
+ set(value) {
81
+ Timber .w(" ## VoiceBroadcastPlayer state: $field -> $value " )
82
+ field = value
83
+ }
84
+ private var currentRoomId: String? = null
58
85
86
+ fun playOrResume (roomId : String , eventId : String ) {
87
+ val hasChanged = currentVoiceBroadcastId != eventId
59
88
when {
60
- currentVoiceBroadcastEventId != eventId -> {
61
- stop()
62
- updatePlaylist(room, eventId)
63
- startPlayback()
64
- }
65
- playbackTracker.getPlaybackState(eventId) is State .Playing -> pause()
66
- else -> resumePlayback()
89
+ hasChanged -> startPlayback(roomId, eventId)
90
+ state == State .PAUSED -> resumePlayback()
91
+ else -> Unit
67
92
}
68
93
}
69
94
70
95
fun pause () {
71
96
currentMediaPlayer?.pause()
72
- currentVoiceBroadcastEventId?.let { playbackTracker.pausePlayback(it) }
97
+ currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
98
+ state = State .PAUSED
73
99
}
74
100
75
101
fun stop () {
102
+ // Stop playback
76
103
currentMediaPlayer?.stop()
77
- currentMediaPlayer?.release()
78
- currentMediaPlayer?.setOnInfoListener(null )
104
+ currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
105
+
106
+ // Release current player
107
+ release(currentMediaPlayer)
79
108
currentMediaPlayer = null
80
- currentVoiceBroadcastEventId?.let { playbackTracker.stopPlayback(it) }
109
+
110
+ // Release next player
111
+ release(nextMediaPlayer)
112
+ nextMediaPlayer = null
113
+
114
+ // Do not observe anymore voice broadcast state changes
115
+ voiceBroadcastStateJob?.cancel()
116
+ voiceBroadcastStateJob = null
117
+
118
+ // In case of live broadcast, stop observing new chunks
119
+ currentTimeline = null
120
+ timelineListener = null
121
+
122
+ // Update state
123
+ state = State .IDLE
124
+
125
+ // Clear playlist
81
126
playlist = emptyList()
82
- currentPlayingIndex = - 1
127
+ currentSequence = null
128
+ currentRoomId = null
83
129
}
84
130
85
- private fun updatePlaylist (room : Room , eventId : String ) {
86
- val timelineEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType .REFERENCE , eventId)
87
- val audioEvents = timelineEvents.mapNotNull { it.root.asMessageAudioEvent() }
88
- playlist = audioEvents.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ? : it.root.originServerTs }
131
+ private fun startPlayback (roomId : String , eventId : String ) {
132
+ val room = session.getRoom(roomId) ? : error(" Unknown roomId: $roomId " )
133
+ currentRoomId = roomId
134
+
135
+ // Stop listening previous voice broadcast if any
136
+ if (state != State .IDLE ) stop()
137
+
138
+ state = State .BUFFERING
139
+
140
+ val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
141
+ if (voiceBroadcastState == VoiceBroadcastState .STOPPED ) {
142
+ // Get static playlist
143
+ updatePlaylist(getExistingChunks(room, eventId))
144
+ startPlayback(false )
145
+ } else {
146
+ playLiveVoiceBroadcast(room, eventId)
147
+ }
89
148
}
90
149
91
- private fun startPlayback () {
92
- val content = playlist.firstOrNull()?.content ? : run { Timber .w(" ## VoiceBroadcastPlayer: No content to play" ); return }
93
- mediaPlayerScope.launch {
150
+ private fun startPlayback (isLive : Boolean ) {
151
+ val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull()
152
+ val content = event?.content ? : run { Timber .w(" ## VoiceBroadcastPlayer: No content to play" ); return }
153
+ val sequence = event.getVoiceBroadcastChunk()?.sequence
154
+ coroutineScope.launch {
94
155
try {
95
156
currentMediaPlayer = prepareMediaPlayer(content)
96
157
currentMediaPlayer?.start()
97
- currentPlayingIndex = 0
98
- currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) }
99
- prepareNextFile()
158
+ currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
159
+ currentSequence = sequence
160
+ state = State .PLAYING
161
+ nextMediaPlayer = prepareNextMediaPlayer()
100
162
} catch (failure: Throwable ) {
101
163
Timber .e(failure, " Unable to start playback" )
102
164
throw VoiceFailure .UnableToPlay (failure)
103
165
}
104
166
}
105
167
}
106
168
169
+ private fun playLiveVoiceBroadcast (room : Room , eventId : String ) {
170
+ room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ? : error(" Cannot retrieve voice broadcast $eventId " )
171
+ updatePlaylist(getExistingChunks(room, eventId))
172
+ startPlayback(true )
173
+ observeIncomingEvents(room, eventId)
174
+ }
175
+
176
+ private fun getExistingChunks (room : Room , eventId : String ): List <MessageAudioEvent > {
177
+ return room.timelineService().getTimelineEventsRelatedTo(RelationType .REFERENCE , eventId)
178
+ .mapNotNull { it.root.asMessageAudioEvent() }
179
+ .filter { it.isVoiceBroadcast() }
180
+ }
181
+
182
+ private fun observeIncomingEvents (room : Room , eventId : String ) {
183
+ currentTimeline = room.timelineService().createTimeline(null , TimelineSettings (5 )).also { timeline ->
184
+ timelineListener = TimelineListener (eventId).also { timeline.addListener(it) }
185
+ timeline.start()
186
+ }
187
+ }
188
+
107
189
private fun resumePlayback () {
108
190
currentMediaPlayer?.start()
109
- currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) }
191
+ currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
192
+ state = State .PLAYING
110
193
}
111
194
112
- private suspend fun prepareNextFile () {
113
- val nextContent = playlist.getOrNull(currentPlayingIndex + 1 )?.content
114
- if (nextContent == null ) {
115
- currentMediaPlayer?.setOnCompletionListener(mediaPlayerListener)
116
- } else {
117
- val nextMediaPlayer = prepareMediaPlayer(nextContent)
118
- currentMediaPlayer?.setNextMediaPlayer(nextMediaPlayer)
119
- }
195
+ private fun updatePlaylist (playlist : List <MessageAudioEvent >) {
196
+ this .playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ? : it.root.originServerTs }
197
+ }
198
+
199
+ private fun getNextAudioContent (): MessageAudioContent ? {
200
+ val nextSequence = currentSequence?.plus(1 )
201
+ ? : timelineListener?.let { playlist.lastOrNull()?.sequence }
202
+ ? : 1
203
+ return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
204
+ }
205
+
206
+ private suspend fun prepareNextMediaPlayer (): MediaPlayer ? {
207
+ val nextContent = getNextAudioContent() ? : return null
208
+ return prepareMediaPlayer(nextContent)
120
209
}
121
210
122
211
private suspend fun prepareMediaPlayer (messageAudioContent : MessageAudioContent ): MediaPlayer {
@@ -140,28 +229,78 @@ class VoiceBroadcastPlayer @Inject constructor(
140
229
setDataSource(fis.fd)
141
230
setOnInfoListener(mediaPlayerListener)
142
231
setOnErrorListener(mediaPlayerListener)
232
+ setOnCompletionListener(mediaPlayerListener)
143
233
prepare()
144
234
}
145
235
}
146
236
}
147
237
148
- inner class MediaPlayerListener : MediaPlayer .OnInfoListener , MediaPlayer .OnCompletionListener , MediaPlayer .OnErrorListener {
238
+ private fun release (mp : MediaPlayer ? ) {
239
+ mp?.apply {
240
+ release()
241
+ setOnInfoListener(null )
242
+ setOnCompletionListener(null )
243
+ setOnErrorListener(null )
244
+ }
245
+ }
246
+
247
+ private inner class TimelineListener (private val voiceBroadcastId : String ) : Timeline.Listener {
248
+ override fun onTimelineUpdated (snapshot : List <TimelineEvent >) {
249
+ val currentSequences = playlist.map { it.sequence }
250
+ val newChunks = snapshot
251
+ .mapNotNull { timelineEvent ->
252
+ timelineEvent.root.asMessageAudioEvent()
253
+ ?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences }
254
+ }
255
+ if (newChunks.isEmpty()) return
256
+ updatePlaylist(playlist + newChunks)
257
+
258
+ when (state) {
259
+ State .PLAYING -> {
260
+ if (nextMediaPlayer == null ) {
261
+ coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
262
+ }
263
+ }
264
+ State .PAUSED -> {
265
+ if (nextMediaPlayer == null ) {
266
+ coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
267
+ }
268
+ }
269
+ State .BUFFERING -> {
270
+ val newMediaContent = getNextAudioContent()
271
+ if (newMediaContent != null ) startPlayback(true )
272
+ }
273
+ State .IDLE -> startPlayback(true )
274
+ }
275
+ }
276
+ }
277
+
278
+ private inner class MediaPlayerListener : MediaPlayer .OnInfoListener , MediaPlayer .OnCompletionListener , MediaPlayer .OnErrorListener {
149
279
150
280
override fun onInfo (mp : MediaPlayer , what : Int , extra : Int ): Boolean {
151
281
when (what) {
152
282
MediaPlayer .MEDIA_INFO_STARTED_AS_NEXT -> {
283
+ release(currentMediaPlayer)
153
284
currentMediaPlayer = mp
154
- currentPlayingIndex ++
155
- mediaPlayerScope .launch { prepareNextFile () }
285
+ currentSequence = currentSequence?.plus( 1 )
286
+ coroutineScope .launch { nextMediaPlayer = prepareNextMediaPlayer () }
156
287
}
157
288
}
158
289
return false
159
290
}
160
291
161
292
override fun onCompletion (mp : MediaPlayer ) {
162
- // Verify that a new media has not been set in the mean time
163
- if (! currentMediaPlayer?.isPlaying.orFalse()) {
293
+ if (nextMediaPlayer != null ) return
294
+ val roomId = currentRoomId ? : return
295
+ val voiceBroadcastId = currentVoiceBroadcastId ? : return
296
+ val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ? : return
297
+ val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState .STOPPED
298
+
299
+ if (! isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
300
+ // We'll not receive new chunks anymore so we can stop the live listening
164
301
stop()
302
+ } else {
303
+ state = State .BUFFERING
165
304
}
166
305
}
167
306
@@ -170,4 +309,11 @@ class VoiceBroadcastPlayer @Inject constructor(
170
309
return true
171
310
}
172
311
}
312
+
313
+ enum class State {
314
+ PLAYING ,
315
+ PAUSED ,
316
+ BUFFERING ,
317
+ IDLE
318
+ }
173
319
}
0 commit comments