-
Notifications
You must be signed in to change notification settings - Fork 171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Receive and play a voice message #1503
Receive and play a voice message #1503
Conversation
📱 Scan the QR code below to install the build (arm64 only) for this PR. |
14f01ee
to
0700c95
Compare
earpiece listening would be nice to have, but works fine here on my fork! |
Thanks for the feedback!!! |
dc91436
to
9f2b1da
Compare
@Composable | ||
inline fun <reified C : TimelineItemEventContent, reified S : Any> TimelineItemPresenterFactories.rememberPresenter( | ||
content: C | ||
): Presenter<S> = remember { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should remember
use key
param?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you mean keying by content
?
Right now every voice message gets its own presenter so I'd think this is working as intended.
Is there anything else I should check to ensure we're fine without a key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more about handling content updates
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
b8f62f5
to
f7f7e8a
Compare
a483409
to
7330dfc
Compare
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## develop #1503 +/- ##
===========================================
+ Coverage 59.22% 59.26% +0.03%
===========================================
Files 1250 1262 +12
Lines 32327 32721 +394
Branches 6633 6715 +82
===========================================
+ Hits 19147 19393 +246
- Misses 10306 10418 +112
- Partials 2874 2910 +36
☔ View full report in Codecov by Sentry. |
2695b13
to
2a1862e
Compare
d0cbf0d
to
c647bc7
Compare
c647bc7
to
f2df7c5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I haven't gotten to review the tests yet. Here's my comments in the mean time
...io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt
Outdated
Show resolved
Hide resolved
...n/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt
Show resolved
Hide resolved
get() = sequenceOf( | ||
aTimelineItemVoiceContent(body = "A sound.mp3"), | ||
aTimelineItemVoiceContent(body = "A bigger name sound.mp3"), | ||
aTimelineItemVoiceContent(body = "An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should we add previews with waveforms containing 0 items and greater than 50 items?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This provider is actually unused, I initially created it to match TimelineItemAudioContentProvider
but then when adding VoiceMessagePresenter
I opted to use VoiceMessageStateProvider
in TimelineItemVoiceView
instead.
Thing is a preview can only use one provider (instead here I'd love to have 2) so I kind of forgot about it over time.
I now created TimelineItemVoiceViewParametersProvider
which combines the 2 so I can use that in the preview instead and added some previews based on your suggestions.
But I still think there is still work to do as refinement here, for instance: should we keep the separate content
object or should we just embed it in the state and only expose the state on the view side? @ganfra I'd like to also have your take on this.
* | ||
* @return the file path of the voice message in the cache directory. | ||
*/ | ||
val cachePath: String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
val cachePath: String | |
val cacheDir: File |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a file path (as in full path to file e.g. /a/b/c/d/something.ogg
) not a directory.
I get it was not clear enough from the docstring, is there a better way to explain it in English?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see - I was a bit confused when I first saw the interface what it's for. I thought this is used to provide access to the file. But I think it's used to create the mediaUri here?
It's not a big problem if we need it in this format - happy to leave as is!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both.
The idea is that we map one mxc:// content uri to one file in the cache via a simple mapping: "${cacheDir.path}/$CACHE_VOICE_SUBDIR/${mxcUri2FilePath(mxcUri)}"
.
So once we have a content uri we can initialize an instance of this class and:
- it will give us the path to the cache file using
cachePath
- it will tell us if the file at such path actually exists with
isInCache()
(i.e. cache hit) or not (i.e. cache miss) - it will give use the option to fill the cache with such content (
moveToCache()
)
Perhaps we can find better method names?
* | ||
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris | ||
*/ | ||
private val mxcRegex = Regex("""^mxc:\/\/([^\/]+)\/([^\/]+)$""") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might this fit better in the libraries:matrix
module?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, that module is only to interface with the rust sdk AFAIK. @bmarty any suggestions?
if (playerState.isMyMedia) { | ||
if (playerState.isPlaying) { | ||
player.pause() | ||
} else { | ||
player.play() | ||
} | ||
} else { | ||
if (voiceCache.isInCache()) { | ||
player.acquireControlAndPlay() | ||
} else { | ||
scope.launch { downloadCacheAndPlay() } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ownership aspect is a lot for the presenter to know about. Could the VoiceMessagePlayer
not take care of some of this logic? Leaving us with something simpler like
if (!isDownloaded) {
download()
}
if (isPlaying) {
pause()
} else {
play()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but I prefer to take some more time aside to make this better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, I don't think it should block this PR
...in/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt
Outdated
Show resolved
Hide resolved
onProgressChangeFinished = { | ||
// This is to send just one onSeek callback after the user has finished seeking. | ||
// Otherwise the AudioWaveform library would send multiple callbacks while the user is seeking. | ||
val p = seekProgress!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of crashing with NPE can we log an error and fail silently?
If it's an important error, we could also track it to Sentry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is my understanding that onProgressChangeFinished
will always be called after at least one invocation of onProgressChange
so NPEs should be impossible here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it certainly is the case, no problem. Personally I would er on the side of caution!
...o/element/android/features/messages/impl/voicemessages/timeline/WaveformProgressIndicator.kt
Outdated
Show resolved
Hide resolved
...impl.timeline.components.event_null_TimelineItemVoiceView-D-39_39_null_1,NEXUS_5,1.0,en].png
Outdated
Show resolved
Hide resolved
Pushed first batch of fixes, there are still a few to go. |
|
Addressed all suggestions except: #1503 (comment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes. I don't think we have any blocking issues here. Looking forward to merging this!
...ges/impl/src/test/kotlin/io/element/android/features/messages/mediaplayer/MediaPlayerFake.kt
Outdated
Show resolved
Hide resolved
...test/kotlin/io/element/android/features/messages/voicemessages/play/VoiceMessageCacheFake.kt
Outdated
Show resolved
Hide resolved
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
Type of change
Content
This PR consists of several macro-blocks separated by path/package:
messages.impl.mediaplayer
: Global (room-wide) media player, now used only for voice messages but could be used for all media within EX in the future. It is backed by media3's exoplayer. Currently not unit-tested because mocking exoplayer is not trivial.messages.impl.voicemessages.play
: Business logic of a timeline voice message. This is all the logic that manages the voice message bubble.messages.impl.timeline.model
&messages.impl.timeline.factories
: Timeline code that takes care of creating thecontent
object for voice messages.messages.impl.timeline.components
: The actual View composable that shows the UI inside a voice message bubble.All the rest is just small related changes that must be done here and there in existing code.
From a high level perspective this is how it works:
VoiceMessageCache
class, it is just a subdirectory in the app's cacheDir which is indexed by the matrix content uri). All further play attempts are done from the cache without hitting the rust SDK anymore.VoiceMessagePlayer
class which is basically a "view" of the globalMediaPlayer
that allow the voice message to only see the media player state belonging to its media content.WaveformProgressIndicator
composable.Known issues:
Motivation and context
element-hq/element-meta#2083
Screenshots / GIFs
Provided by Screenshot tests in the PR itself.