mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-01 14:16:49 +00:00
Implement correct video story sound behaviour.
This commit is contained in:
committed by
Cody Henthorne
parent
521bd2cce4
commit
1cfa5c31f2
@@ -10,6 +10,7 @@ class VideoControlsDelegate {
|
||||
|
||||
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
|
||||
private var player: Player? = null
|
||||
private var isMuted: Boolean = true
|
||||
|
||||
fun getPlayerState(uri: Uri): PlayerState? {
|
||||
val player: Player? = this.player
|
||||
@@ -41,9 +42,29 @@ class VideoControlsDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fun mute() {
|
||||
isMuted = true
|
||||
player?.videoPlayer?.mute()
|
||||
}
|
||||
|
||||
fun unmute() {
|
||||
isMuted = false
|
||||
player?.videoPlayer?.unmute()
|
||||
}
|
||||
|
||||
fun hasAudioStream(): Boolean {
|
||||
return player?.videoPlayer?.hasAudioTrack() ?: false
|
||||
}
|
||||
|
||||
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?, isGif: Boolean) {
|
||||
player = Player(uri, videoPlayer, isGif)
|
||||
|
||||
if (isMuted) {
|
||||
videoPlayer?.mute()
|
||||
} else {
|
||||
videoPlayer?.unmute()
|
||||
}
|
||||
|
||||
if (playWhenReady[uri] == true) {
|
||||
playWhenReady[uri] = false
|
||||
videoPlayer?.play()
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.withClip
|
||||
import androidx.media.AudioManagerCompat
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
|
||||
/**
|
||||
* Displays a vertical volume bar.
|
||||
*/
|
||||
class StoryVolumeBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val minimum: Int
|
||||
private val maximum: Int
|
||||
|
||||
private var level: Int
|
||||
private val bounds: Rect = Rect()
|
||||
private val clipBoundsF: RectF = RectF()
|
||||
private val clipPath: Path = Path()
|
||||
private val clipRadius = DimensionUnit.DP.toPixels(4f)
|
||||
|
||||
private val backgroundPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.transparent_black_40)
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val foregroundPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.core_white)
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
init {
|
||||
val audioManager = ServiceUtil.getAudioManager(context)
|
||||
|
||||
if (isInEditMode) {
|
||||
minimum = 0
|
||||
maximum = 100
|
||||
level = 50
|
||||
} else {
|
||||
minimum = AudioManagerCompat.getStreamMinVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
maximum = AudioManagerCompat.getStreamMaxVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
level = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
}
|
||||
}
|
||||
|
||||
fun setLevel(level: Int) {
|
||||
this.level = level
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
backgroundPaint.strokeWidth = (measuredWidth - paddingLeft - paddingRight).toFloat()
|
||||
foregroundPaint.strokeWidth = backgroundPaint.strokeWidth
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.getClipBounds(bounds)
|
||||
clipBoundsF.set(bounds)
|
||||
clipPath.reset()
|
||||
clipPath.addRoundRect(clipBoundsF, clipRadius, clipRadius, Path.Direction.CW)
|
||||
|
||||
canvas.withClip(clipPath) {
|
||||
canvas.drawLine(bounds.exactCenterX(), bounds.top.toFloat(), bounds.exactCenterX(), bounds.bottom.toFloat(), backgroundPaint)
|
||||
|
||||
val fillPercent: Float = (level - minimum) / (maximum - minimum.toFloat())
|
||||
val fillHeight = bounds.height() * fillPercent
|
||||
|
||||
canvas.drawLine(bounds.exactCenterX(), bounds.bottom.toFloat() - fillHeight, bounds.exactCenterX(), bounds.bottom.toFloat(), foregroundPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Displays a volume bar along with an indicator specifiying whether or not
|
||||
* a given video contains sound.
|
||||
*/
|
||||
class StoryVolumeOverlayView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : ConstraintLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.story_volume_overlay_view, this)
|
||||
}
|
||||
|
||||
private val videoHasNoAudioIndicator: View = findViewById(R.id.story_no_audio_indicator)
|
||||
private val volumeBar: StoryVolumeBar = findViewById(R.id.story_volume_bar)
|
||||
|
||||
fun setVideoHaNoAudio(videoHasNoAudio: Boolean) {
|
||||
videoHasNoAudioIndicator.visible = videoHasNoAudio
|
||||
}
|
||||
|
||||
fun setVolumeLevel(level: Int) {
|
||||
volumeBar.setLevel(level)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver
|
||||
|
||||
/**
|
||||
* Stories are to start muted, and once unmuted, remain as such until the
|
||||
* user backgrounds the application.
|
||||
*/
|
||||
object StoryMutePolicy : AppForegroundObserver.Listener {
|
||||
var isContentMuted: Boolean = true
|
||||
|
||||
fun initialize() {
|
||||
ApplicationDependencies.getAppForegroundObserver().addListener(this)
|
||||
}
|
||||
|
||||
override fun onBackground() {
|
||||
isContentMuted = true
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,26 @@ package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.media.AudioManagerCompat
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.stories.StoryViewerArgs
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner {
|
||||
|
||||
private val viewModel: StoryVolumeViewModel by viewModels()
|
||||
|
||||
override lateinit var voiceNoteMediaController: VoiceNoteMediaController
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
@@ -21,6 +30,8 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
StoryMutePolicy.initialize()
|
||||
|
||||
supportPostponeEnterTransition()
|
||||
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
@@ -33,6 +44,15 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (StoryMutePolicy.isContentMuted) {
|
||||
viewModel.mute()
|
||||
} else {
|
||||
viewModel.unmute()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
@@ -54,6 +74,25 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val audioManager = ServiceUtil.getAudioManager(this)
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
val maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
StoryMutePolicy.isContentMuted = false
|
||||
viewModel.onVolumeUp(min(maxVolume, audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + 1))
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
val minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, AudioManager.STREAM_MUSIC)
|
||||
viewModel.onVolumeDown(max(minVolume, audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - 1))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGS = "story.viewer.args"
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
data class StoryVolumeState(
|
||||
val isMuted: Boolean = true,
|
||||
val level: Int = -1
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class StoryVolumeViewModel : ViewModel() {
|
||||
private val store = RxStore(StoryVolumeState())
|
||||
|
||||
val state: Flowable<StoryVolumeState> = store.stateFlowable
|
||||
val snapshot: StoryVolumeState get() = store.state
|
||||
|
||||
fun mute() {
|
||||
store.update { it.copy(isMuted = true) }
|
||||
}
|
||||
|
||||
fun unmute() {
|
||||
store.update { it.copy(isMuted = false) }
|
||||
}
|
||||
|
||||
fun onVolumeDown(level: Int) {
|
||||
store.update { it.copy(level = level) }
|
||||
}
|
||||
|
||||
fun onVolumeUp(level: Int) {
|
||||
store.update { it.copy(isMuted = false, level = level) }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
@@ -7,6 +8,7 @@ import android.content.Context
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -54,9 +56,11 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
|
||||
import org.thoughtcrime.securesms.stories.StorySlateView
|
||||
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerState
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.StoriesSharedElementCrossFaderView
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.group.StoryGroupReplyBottomSheetDialogFragment
|
||||
@@ -67,7 +71,9 @@ import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsBottomSheetDial
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
||||
@@ -87,6 +93,8 @@ class StoryViewerPageFragment :
|
||||
StoriesSharedElementCrossFaderView.Callback,
|
||||
StoryFirstTimeNavigationView.Callback {
|
||||
|
||||
private val activityViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
|
||||
private lateinit var progressBar: SegmentedProgressBar
|
||||
private lateinit var storySlate: StorySlateView
|
||||
private lateinit var viewsAndReplies: MaterialButton
|
||||
@@ -101,6 +109,10 @@ class StoryViewerPageFragment :
|
||||
private lateinit var chrome: List<View>
|
||||
private var animatorSet: AnimatorSet? = null
|
||||
|
||||
private var volumeInAnimator: Animator? = null
|
||||
private var volumeOutAnimator: Animator? = null
|
||||
private var volumeDebouncer: Debouncer = Debouncer(3, TimeUnit.SECONDS)
|
||||
|
||||
private val viewModel: StoryViewerPageViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
StoryViewerPageViewModel.Factory(storyRecipientId, initialStoryId, isUnviewedOnly, StoryViewerPageRepository(requireContext()))
|
||||
@@ -135,6 +147,12 @@ class StoryViewerPageFragment :
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = requireListener()
|
||||
|
||||
if (activityViewModel.snapshot.isMuted) {
|
||||
videoControlsDelegate.mute()
|
||||
} else {
|
||||
videoControlsDelegate.unmute()
|
||||
}
|
||||
|
||||
val closeView: View = view.findViewById(R.id.close)
|
||||
val senderAvatar: AvatarImageView = view.findViewById(R.id.sender_avatar)
|
||||
val groupAvatar: AvatarImageView = view.findViewById(R.id.group_avatar)
|
||||
@@ -150,6 +168,7 @@ class StoryViewerPageFragment :
|
||||
val reactionAnimationView: OnReactionSentView = view.findViewById(R.id.on_reaction_sent_view)
|
||||
val storyGradientTop: View = view.findViewById(R.id.story_gradient_top)
|
||||
val storyGradientBottom: View = view.findViewById(R.id.story_gradient_bottom)
|
||||
val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay)
|
||||
|
||||
blurContainer = view.findViewById(R.id.story_blur_container)
|
||||
storyContentContainer = view.findViewById(R.id.story_content_container)
|
||||
@@ -267,6 +286,29 @@ class StoryViewerPageFragment :
|
||||
viewModel.setIsUserScrollingParent(isScrolling)
|
||||
}
|
||||
|
||||
lifecycleDisposable += activityViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { volumeState ->
|
||||
if (volumeState.isMuted) {
|
||||
videoControlsDelegate.mute()
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!viewModel.hasPost() || !viewModel.getPost().content.isVideo() || volumeState.level < 0) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!volumeState.isMuted) {
|
||||
videoControlsDelegate.unmute()
|
||||
}
|
||||
|
||||
val audioManager = ServiceUtil.getAudioManager(requireContext())
|
||||
if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) != volumeState.level) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volumeState.level, 0)
|
||||
storyVolumeOverlayView.setVolumeLevel(volumeState.level)
|
||||
storyVolumeOverlayView.setVideoHaNoAudio(!videoControlsDelegate.hasAudioStream())
|
||||
displayStoryVolumeOverlayForTimeout(storyVolumeOverlayView)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += sharedViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { parentState ->
|
||||
if (parentState.pages.size <= parentState.page) {
|
||||
viewModel.setIsSelectedPage(false)
|
||||
@@ -421,6 +463,8 @@ class StoryViewerPageFragment :
|
||||
it.cleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
volumeDebouncer.clear()
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
@@ -455,6 +499,26 @@ class StoryViewerPageFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayStoryVolumeOverlayForTimeout(view: View) {
|
||||
if (volumeInAnimator?.isRunning != true) {
|
||||
volumeOutAnimator?.cancel()
|
||||
volumeInAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1f).apply {
|
||||
duration = 200
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
volumeDebouncer.publish {
|
||||
if (volumeOutAnimator?.isRunning != true) {
|
||||
volumeInAnimator?.cancel()
|
||||
volumeOutAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f).apply {
|
||||
duration = 200
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideChromeImmediate() {
|
||||
animatorSet?.cancel()
|
||||
chrome.map {
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.util;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A class that will throttle the number of runnables executed to be at most once every specified
|
||||
* interval. However, it could be longer if events are published consistently.
|
||||
@@ -17,6 +19,10 @@ public class Debouncer {
|
||||
private final Handler handler;
|
||||
private final long threshold;
|
||||
|
||||
public Debouncer(long threshold, TimeUnit timeUnit) {
|
||||
this(timeUnit.toMillis(threshold));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
|
||||
* {@code threshold} milliseconds.
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
@@ -41,6 +42,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -136,6 +138,36 @@ public class VideoPlayer extends FrameLayout {
|
||||
exoPlayer.setPlayWhenReady(autoplay);
|
||||
}
|
||||
|
||||
public void mute() {
|
||||
if (exoPlayer != null && exoPlayer.getAudioComponent() != null) {
|
||||
exoPlayer.getAudioComponent().setVolume(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void unmute() {
|
||||
if (exoPlayer != null && exoPlayer.getAudioComponent() != null) {
|
||||
exoPlayer.getAudioComponent().setVolume(1f);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAudioTrack() {
|
||||
if (exoPlayer != null) {
|
||||
TrackGroupArray trackGroupArray = exoPlayer.getCurrentTrackGroups();
|
||||
if (trackGroupArray != null) {
|
||||
for (int i = 0; i < trackGroupArray.length; i++) {
|
||||
for (int j = 0; j < trackGroupArray.get(i).length; j++) {
|
||||
String sampleMimeType = trackGroupArray.get(i).getFormat(j).sampleMimeType;
|
||||
if (MediaUtil.isAudioType(sampleMimeType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return exoPlayer != null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user