mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Add duration extraction for audio files
This commit is contained in:
@@ -104,7 +104,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
viewHolder.audioView.setVisibility(View.VISIBLE);
|
||||
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide);
|
||||
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
viewHolder.audioView.disablePlayer(!selected.isEmpty());
|
||||
|
||||
@@ -493,12 +493,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE);
|
||||
if (callViewStub.resolved()) callViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
int duration = messageRecord.getDuration();
|
||||
|
||||
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
|
||||
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord));
|
||||
audioViewStub.get().setOnClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
|
||||
+67
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -13,6 +15,13 @@ import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.session.MediaController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
||||
public class AudioPlaybackViewModel extends ViewModel {
|
||||
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
|
||||
@@ -20,6 +29,11 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id.
|
||||
|
||||
private final MutableLiveData<AudioPlaybackState> playbackState;
|
||||
|
||||
private final MutableLiveData<Map<Integer, Long>> durations = new MutableLiveData<>(new HashMap<>());
|
||||
private final Set<Integer> extractionInProgress = new HashSet<>();
|
||||
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private final Handler handler;
|
||||
private boolean isUserSeeking = false;
|
||||
@@ -73,6 +87,58 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
|
||||
}
|
||||
|
||||
public LiveData<Map<Integer, Long>> getDurations() {
|
||||
return durations;
|
||||
}
|
||||
|
||||
public void ensureDurationLoaded(Context context, int msgId, Uri audioUri) {
|
||||
// Check cache
|
||||
Map<Integer, Long> currentDurations = durations.getValue();
|
||||
if (currentDurations != null && currentDurations.containsKey(msgId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check extracting
|
||||
synchronized (extractionInProgress) {
|
||||
if (extractionInProgress.contains(msgId)) {
|
||||
return;
|
||||
}
|
||||
extractionInProgress.add(msgId);
|
||||
}
|
||||
|
||||
// Extract in background
|
||||
extractionExecutor.execute(() -> {
|
||||
long duration = extractDurationFromAudio(context, audioUri);
|
||||
|
||||
handler.post(() -> {
|
||||
Map<Integer, Long> updatedDurations = new HashMap<>(durations.getValue());
|
||||
updatedDurations.put(msgId, duration);
|
||||
durations.setValue(updatedDurations);
|
||||
});
|
||||
|
||||
synchronized (extractionInProgress) {
|
||||
extractionInProgress.remove(msgId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private long extractDurationFromAudio(Context context, Uri audioUri) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(context, audioUri);
|
||||
String durationStr = retriever.extractMetadata(
|
||||
MediaMetadataRetriever.METADATA_KEY_DURATION
|
||||
);
|
||||
return durationStr != null ? Long.parseLong(durationStr) : 0;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
} finally {
|
||||
try {
|
||||
retriever.release();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void pause(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.pause();
|
||||
@@ -238,6 +304,7 @@ public class AudioPlaybackViewModel extends ViewModel {
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
stopUpdateProgress();
|
||||
extractionExecutor.shutdown();
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class AudioView extends FrameLayout {
|
||||
|
||||
@@ -39,10 +41,13 @@ public class AudioView extends FrameLayout {
|
||||
private final @NonNull View mask;
|
||||
private OnActionListener listener;
|
||||
|
||||
private int msgId;
|
||||
private int msgId = -1;
|
||||
private Uri audioUri;
|
||||
private int progress;
|
||||
private int duration;
|
||||
private AudioPlaybackViewModel viewModel;
|
||||
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
|
||||
private final Observer<Map<Integer, Long>> durationObserver = this::onDurationsChanged;
|
||||
private boolean isPlaying;
|
||||
|
||||
public AudioView(Context context) {
|
||||
@@ -63,7 +68,7 @@ public class AudioView extends FrameLayout {
|
||||
this.title = findViewById(R.id.title);
|
||||
this.mask = findViewById(R.id.interception_mask);
|
||||
|
||||
this.timestamp.setText("00:00");
|
||||
updateTimestamps();
|
||||
|
||||
// Load drawables once
|
||||
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
|
||||
@@ -94,6 +99,9 @@ public class AudioView extends FrameLayout {
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
viewModel.getPlaybackState().observeForever(stateObserver);
|
||||
|
||||
viewModel.getDurations().removeObserver(durationObserver);
|
||||
viewModel.getDurations().observeForever(durationObserver);
|
||||
}
|
||||
|
||||
playPauseButton.setOnClickListener(v -> {
|
||||
@@ -125,7 +133,8 @@ public class AudioView extends FrameLayout {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
timestamp.setText(DateUtils.getFormatedDuration(progress));
|
||||
AudioView.this.progress = progress;
|
||||
updateTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +162,7 @@ public class AudioView extends FrameLayout {
|
||||
protected void onDetachedFromWindow() {
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
viewModel.getDurations().removeObserver(durationObserver);
|
||||
}
|
||||
if (playToPauseDrawable != null) {
|
||||
playToPauseDrawable.clearAnimationCallbacks();
|
||||
@@ -166,6 +176,7 @@ public class AudioView extends FrameLayout {
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
|
||||
if (this.viewModel != null) {
|
||||
this.viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
this.viewModel.getDurations().removeObserver(durationObserver);
|
||||
}
|
||||
|
||||
// ViewModel is used directly for simplicity, since there is no reuse yet
|
||||
@@ -173,10 +184,11 @@ public class AudioView extends FrameLayout {
|
||||
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().observeForever(stateObserver);
|
||||
viewModel.getDurations().observeForever(durationObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio, int duration)
|
||||
public void setAudio(final @NonNull AudioSlide audio)
|
||||
{
|
||||
msgId = audio.getDcMsgId();
|
||||
audioUri = audio.getUri();
|
||||
@@ -184,7 +196,15 @@ public class AudioView extends FrameLayout {
|
||||
|
||||
seekBar.setEnabled(true);
|
||||
seekBar.setProgress(0);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
|
||||
// Get duration
|
||||
Map<Integer, Long> durations = viewModel.getDurations().getValue();
|
||||
if (durations != null && durations.containsKey(msgId)) {
|
||||
this.duration = Math.toIntExact(durations.get(msgId));
|
||||
updateTimestamps();
|
||||
} else {
|
||||
viewModel.ensureDurationLoaded(getContext(), msgId, audioUri);
|
||||
}
|
||||
|
||||
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
@@ -243,13 +263,15 @@ public class AudioView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void updateProgress(AudioPlaybackState state) {
|
||||
int duration = (int) state.getDuration();
|
||||
int position = (int) state.getCurrentPosition();
|
||||
int duration = Math.toIntExact(state.getDuration());
|
||||
int position = Math.toIntExact(state.getCurrentPosition());
|
||||
|
||||
if (duration > 0) {
|
||||
seekBar.setMax(duration);
|
||||
this.progress = position;
|
||||
this.duration = duration;
|
||||
updateTimestamps();
|
||||
seekBar.setProgress(position);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(position));
|
||||
seekBar.setMax(duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,4 +337,28 @@ public class AudioView extends FrameLayout {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onDurationsChanged(Map<Integer, Long> durations) {
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
// When there is no playback happening, msgId can be -1 and audioUri is null
|
||||
if (state != null &&
|
||||
msgId >= 0 && msgId == state.getMsgId() &&
|
||||
audioUri != null && audioUri.equals(state.getAudioUri())) {
|
||||
return; // Is playing this message
|
||||
}
|
||||
|
||||
Long duration = durations.get(msgId);
|
||||
if (duration != null && seekBar.getMax() <= 100) {
|
||||
this.duration = Math.toIntExact(duration);
|
||||
updateTimestamps();
|
||||
seekBar.setMax(this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimestamps() {
|
||||
String progressText = DateUtils.getFormatedDuration(progress);
|
||||
String durationText = DateUtils.getFormatedDuration(duration);
|
||||
timestamp.setText(String.format("%s / %s", progressText, durationText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ public class AttachmentManager {
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
audioView.setPlaybackViewModel(playbackViewModel);
|
||||
audioView.setAudio((AudioSlide) slide, 0);
|
||||
audioView.setAudio((AudioSlide) slide);
|
||||
removableMediaView.display(audioView, false);
|
||||
removableMediaView.addRemoveClickListener(v -> {
|
||||
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
|
||||
|
||||
Reference in New Issue
Block a user