Added seek to recordings player & media player

This commit is contained in:
Sylvain Berfini 2025-12-05 09:58:09 +01:00
parent 40d195e06b
commit 61c79a86f7
10 changed files with 122 additions and 22 deletions

View file

@ -15,6 +15,7 @@ Group changes to describe their impact on the project, as follows:
### Added
- Added the ability to edit/delete chat messages sent less than 24 hours ago.
- Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it
- Added seeking feature to recordings & media player within app
- Added PDF preview in conversation (message bubble & documents list)
- Added hover effect when using a mouse (useful for tablets or devices with desktop mode)
- Support right click on some items to open bottom sheet/menu

View file

@ -26,6 +26,7 @@ import android.view.Surface
import android.view.TextureView.SurfaceTextureListener
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import org.linphone.core.tools.Log
@ -45,6 +46,21 @@ class MediaViewerFragment : GenericMainFragment() {
private lateinit var viewModel: MediaViewModel
private val seekBarListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
viewModel.pause()
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
val newPosition = seekBar.progress
viewModel.seekTo(newPosition)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -87,6 +103,8 @@ class MediaViewerFragment : GenericMainFragment() {
sharedViewModel.mediaViewerFullScreenMode.value = fullScreenMode
}
binding.setSeekBarListener(seekBarListener)
viewModel.videoSizeChangedEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val width = pair.first

View file

@ -161,6 +161,14 @@ class MediaViewModel
}
}
@UiThread
fun seekTo(position: Int) {
if (::mediaPlayer.isInitialized) {
mediaPlayer.seekTo(position)
play()
}
}
@UiThread
private fun initMediaPlayer() {
isMediaPlaying.value = false

View file

@ -27,6 +27,7 @@ import android.view.LayoutInflater
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -52,6 +53,21 @@ class RecordingMediaPlayerFragment : GenericMainFragment() {
private lateinit var viewModel: RecordingMediaPlayerViewModel
private val seekBarListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
viewModel.pause()
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
val newPosition = seekBar.progress
viewModel.seekTo(newPosition)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -84,6 +100,8 @@ class RecordingMediaPlayerFragment : GenericMainFragment() {
exportFile(viewModel.recordingModel.filePath)
}
binding.setSeekBarListener(seekBarListener)
val model = sharedViewModel.playingRecording
if (model != null) {
Log.i("$TAG Loading recording [${model.fileName}] from shared view model")

View file

@ -175,6 +175,14 @@ class RecordingMediaPlayerViewModel
}
}
@UiThread
fun seekTo(position: Int) {
coreContext.postOnCoreThread {
seekPlaybackTo(position)
startPlayback()
}
}
@WorkerThread
private fun startPlayback() {
if (!::player.isInitialized) return
@ -232,6 +240,18 @@ class RecordingMediaPlayerViewModel
updatePositionJob = null
}
@WorkerThread
private fun seekPlaybackTo(position: Int) {
if (!::player.isInitialized) return
if (player.state == Player.State.Closed) {
player.open(recordingModel.filePath)
}
Log.i("$TAG Seeking player to position [$position]")
player.seek(position)
}
@WorkerThread
private fun stop() {
if (!::player.isInitialized) return

View file

@ -32,6 +32,7 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.SeekBar
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
@ -633,6 +634,11 @@ fun setFlexboxLayoutWrapBefore(view: View, wrap: Boolean = false) {
view.layoutParams = params
}
@BindingAdapter("seekBarListener")
fun setSeekBarListener(seekBar: SeekBar, listener: SeekBar.OnSeekBarChangeListener) {
seekBar.setOnSeekBarChangeListener(listener)
}
@BindingAdapter("emojiPickedListener")
fun EmojiPickerView.setEmojiPickedListener(listener: EmojiPickedListener) {
setOnEmojiPickedListener { emoji ->

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="center_vertical">
<shape android:shape="rectangle">
<solid android:color="?attr/color_main1_100" />
<size android:height="10dp" />
<corners android:radius="5dp" />
</shape>
</item>
<item android:gravity="center_vertical">
<scale android:scaleWidth="100%">
<shape android:shape="rectangle">
<solid android:color="?attr/color_main1_500" />
<size android:height="10dp" />
<corners android:radius="5dp" />
</shape>
</scale>
</item>
</layer-list>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="@color/bc_white" />
<size android:height="20dp" android:width="20dp" />
<corners android:radius="20dp" />
</shape>
</item>
</layer-list>

View file

@ -5,9 +5,13 @@
<data>
<import type="android.view.View" />
<import type="android.widget.SeekBar" />
<variable
name="toggleFullScreenModeClickListener"
type="View.OnClickListener" />
<variable
name="seekBarListener"
type="SeekBar.OnSeekBarChangeListener" />
<variable
name="viewModel"
type="org.linphone.ui.fileviewer.viewmodel.MediaViewModel" />
@ -75,24 +79,20 @@
app:layout_constraintStart_toStartOf="parent"
app:tint="@color/bc_white"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
<SeekBar
seekBarListener="@{seekBarListener}"
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_height="0dp"
android:splitTrack="false"
android:max="@{viewModel.duration, default=100}"
android:progress="@{viewModel.position, default=75}"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
app:trackColor="?attr/color_main1_100"
app:indicatorColor="?attr/color_main1_500"
app:trackStopIndicatorSize="0dp"
app:indicatorTrackGapSize="0dp"
android:progressDrawable="@drawable/media_player_seekbar"
android:thumb="@drawable/media_player_seekbar_thumb"
app:layout_constraintTop_toTopOf="@id/play_pause_audio_playback"
app:layout_constraintBottom_toBottomOf="@id/play_pause_audio_playback"
app:layout_constraintStart_toEndOf="@id/play_pause_audio_playback"
app:layout_constraintEnd_toStartOf="@id/duration"/>
app:layout_constraintEnd_toStartOf="@id/duration" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"

View file

@ -5,6 +5,7 @@
<data>
<import type="android.view.View" />
<import type="android.widget.SeekBar" />
<variable
name="backClickListener"
type="View.OnClickListener" />
@ -14,6 +15,9 @@
<variable
name="exportClickListener"
type="View.OnClickListener" />
<variable
name="seekBarListener"
type="SeekBar.OnSeekBarChangeListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.recordings.viewmodel.RecordingMediaPlayerViewModel" />
@ -62,24 +66,20 @@
app:layout_constraintStart_toStartOf="parent"
app:tint="@color/bc_white"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
<SeekBar
seekBarListener="@{seekBarListener}"
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_height="0dp"
android:splitTrack="false"
android:max="@{viewModel.duration, default=100}"
android:progress="@{viewModel.position, default=75}"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
app:trackColor="?attr/color_main1_100"
app:indicatorColor="?attr/color_main1_500"
app:trackStopIndicatorSize="0dp"
app:indicatorTrackGapSize="0dp"
android:progressDrawable="@drawable/media_player_seekbar"
android:thumb="@drawable/media_player_seekbar_thumb"
app:layout_constraintTop_toTopOf="@id/play_pause_audio_playback"
app:layout_constraintBottom_toBottomOf="@id/play_pause_audio_playback"
app:layout_constraintStart_toEndOf="@id/play_pause_audio_playback"
app:layout_constraintEnd_toStartOf="@id/duration"/>
app:layout_constraintEnd_toStartOf="@id/duration" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"