@@ -18,105 +18,194 @@ package im.vector.app.features.voicebroadcast
1818
1919import android.media.AudioAttributes
2020import android.media.MediaPlayer
21+ import im.vector.app.core.di.ActiveSessionHolder
2122import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
22- import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
2323import 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
2427import kotlinx.coroutines.CoroutineScope
2528import kotlinx.coroutines.Dispatchers
29+ import kotlinx.coroutines.Job
30+ import kotlinx.coroutines.SupervisorJob
2631import kotlinx.coroutines.launch
27- import org.matrix.android.sdk.api.extensions.orFalse
28- import org.matrix.android.sdk.api.session.Session
2932import org.matrix.android.sdk.api.session.events.model.RelationType
3033import org.matrix.android.sdk.api.session.events.model.getRelationContent
3134import org.matrix.android.sdk.api.session.getRoom
3235import org.matrix.android.sdk.api.session.room.Room
3336import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
3437import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
3538import 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
3642import timber.log.Timber
3743import javax.inject.Inject
3844import javax.inject.Singleton
3945
4046@Singleton
4147class VoiceBroadcastPlayer @Inject constructor(
42- private val session : Session ,
48+ private val sessionHolder : ActiveSessionHolder ,
4349 private val playbackTracker : AudioMessagePlaybackTracker ,
50+ private val getVoiceBroadcastUseCase : GetVoiceBroadcastUseCase ,
4451) {
52+ private val session
53+ get() = sessionHolder.getActiveSession()
4554
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
4766
4867 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+
5075 private var playlist = emptyList<MessageAudioEvent >()
51- private val currentVoiceBroadcastEventId
76+ private val currentVoiceBroadcastId
5277 get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId
5378
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
5885
86+ fun playOrResume (roomId : String , eventId : String ) {
87+ val hasChanged = currentVoiceBroadcastId != eventId
5988 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
6792 }
6893 }
6994
7095 fun pause () {
7196 currentMediaPlayer?.pause()
72- currentVoiceBroadcastEventId?.let { playbackTracker.pausePlayback(it) }
97+ currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
98+ state = State .PAUSED
7399 }
74100
75101 fun stop () {
102+ // Stop playback
76103 currentMediaPlayer?.stop()
77- currentMediaPlayer?.release()
78- currentMediaPlayer?.setOnInfoListener(null )
104+ currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
105+
106+ // Release current player
107+ release(currentMediaPlayer)
79108 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
81126 playlist = emptyList()
82- currentPlayingIndex = - 1
127+ currentSequence = null
128+ currentRoomId = null
83129 }
84130
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+ }
89148 }
90149
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 {
94155 try {
95156 currentMediaPlayer = prepareMediaPlayer(content)
96157 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()
100162 } catch (failure: Throwable ) {
101163 Timber .e(failure, " Unable to start playback" )
102164 throw VoiceFailure .UnableToPlay (failure)
103165 }
104166 }
105167 }
106168
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+
107189 private fun resumePlayback () {
108190 currentMediaPlayer?.start()
109- currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) }
191+ currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
192+ state = State .PLAYING
110193 }
111194
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)
120209 }
121210
122211 private suspend fun prepareMediaPlayer (messageAudioContent : MessageAudioContent ): MediaPlayer {
@@ -140,28 +229,78 @@ class VoiceBroadcastPlayer @Inject constructor(
140229 setDataSource(fis.fd)
141230 setOnInfoListener(mediaPlayerListener)
142231 setOnErrorListener(mediaPlayerListener)
232+ setOnCompletionListener(mediaPlayerListener)
143233 prepare()
144234 }
145235 }
146236 }
147237
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 {
149279
150280 override fun onInfo (mp : MediaPlayer , what : Int , extra : Int ): Boolean {
151281 when (what) {
152282 MediaPlayer .MEDIA_INFO_STARTED_AS_NEXT -> {
283+ release(currentMediaPlayer)
153284 currentMediaPlayer = mp
154- currentPlayingIndex ++
155- mediaPlayerScope .launch { prepareNextFile () }
285+ currentSequence = currentSequence?.plus( 1 )
286+ coroutineScope .launch { nextMediaPlayer = prepareNextMediaPlayer () }
156287 }
157288 }
158289 return false
159290 }
160291
161292 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
164301 stop()
302+ } else {
303+ state = State .BUFFERING
165304 }
166305 }
167306
@@ -170,4 +309,11 @@ class VoiceBroadcastPlayer @Inject constructor(
170309 return true
171310 }
172311 }
312+
313+ enum class State {
314+ PLAYING ,
315+ PAUSED ,
316+ BUFFERING ,
317+ IDLE
318+ }
173319}
0 commit comments