diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index b09a5f1d8f7a..74753ea95a44 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -803,7 +803,7 @@ abstract class AbstractFlashcardViewer : "AbstractFlashcardViewer:: OK button pressed to delete note %d", currentCard!!.nid, ) - launchCatchingTask { cardMediaPlayer.stopSounds() } + launchCatchingTask { cardMediaPlayer.stop() } deleteNoteWithoutConfirmation() } negativeButton(R.string.dialog_cancel) @@ -845,7 +845,7 @@ abstract class AbstractFlashcardViewer : } // Temporarily sets the answer indicator dots appearing below the toolbar previousAnswerIndicator?.displayAnswerIndicator(ease) - cardMediaPlayer.stopSounds() + cardMediaPlayer.stop() currentEase = ease answerCardInner(ease) @@ -1286,7 +1286,7 @@ abstract class AbstractFlashcardViewer : } } - private suspend fun automaticAnswerShouldWaitForAudio(): Boolean = + private suspend fun automaticAnswerShouldWaitForMedia(): Boolean = withCol { decks.configDictForDeckId(currentCard!!.did).waitForAudio } @@ -1294,7 +1294,7 @@ abstract class AbstractFlashcardViewer : internal inner class ReadTextListener : ReadText.ReadTextListener { override fun onDone(playedSide: CardSide?) { Timber.d("done reading text") - this@AbstractFlashcardViewer.onSoundGroupCompleted() + this@AbstractFlashcardViewer.onMediaGroupCompleted() } } @@ -1315,7 +1315,7 @@ abstract class AbstractFlashcardViewer : val content = cardRenderContext!!.renderCard(getColUnsafe, currentCard!!, SingleCardSide.FRONT) automaticAnswer.onDisplayQuestion() launchCatchingTask { - if (!automaticAnswerShouldWaitForAudio()) { + if (!automaticAnswerShouldWaitForMedia()) { automaticAnswer.scheduleAutomaticDisplayAnswer() } } @@ -1361,7 +1361,7 @@ abstract class AbstractFlashcardViewer : val answerContent = cardRenderContext!!.renderCard(getColUnsafe, currentCard!!, SingleCardSide.BACK) automaticAnswer.onDisplayAnswer() launchCatchingTask { - if (!automaticAnswerShouldWaitForAudio()) { + if (!automaticAnswerShouldWaitForMedia()) { automaticAnswer.scheduleAutomaticDisplayQuestion() } } @@ -1423,36 +1423,36 @@ abstract class AbstractFlashcardViewer : Timber.d("updateCard()") // TODO: This doesn't need to be blocking runBlocking { - cardMediaPlayer.loadCardSounds(currentCard!!) + cardMediaPlayer.loadCardAvTags(currentCard!!) } cardContent = content.html fillFlashcard() - playSounds(false) // Play sounds if appropriate + playMedia(false) // Play media if appropriate } /** - * Plays sounds (or TTS, if configured) for currently shown side of card. + * Plays media (or TTS, if configured) for currently shown side of card. * - * @param doAudioReplay indicates an anki desktop-like replay call is desired, whose behavior is identical to + * @param doMediaReplay indicates an anki desktop-like replay call is desired, whose behavior is identical to * pressing the keyboard shortcut R on the desktop */ - @NeedsTest("audio is not played if opExecuted occurs when viewer is in the background") - protected open fun playSounds(doAudioReplay: Boolean) { + @NeedsTest("media is not played if opExecuted occurs when viewer is in the background") + protected open fun playMedia(doMediaReplay: Boolean) { // this can occur due to OpChanges when the viewer is on another screen if (!this.lifecycle.currentState.isAtLeast(RESUMED)) { - Timber.w("sounds are not played as the activity is inactive") + Timber.w("media is not played as the activity is inactive") return } - if (!cardMediaPlayer.config.autoplay && !doAudioReplay) return - // Use TTS if TTS preference enabled and no other sound source - val useTTS = tts.enabled && !cardMediaPlayer.hasSounds(displayAnswer) - // We need to play the sounds from the proper side of the card + if (!cardMediaPlayer.config.autoplay && !doMediaReplay) return + // Use TTS if TTS preference enabled and no other media source + val useTTS = tts.enabled && !cardMediaPlayer.hasMedia(displayAnswer) + // We need to play the media from the proper side of the card if (!useTTS) { launchCatchingTask { val side = if (displayAnswer) SingleCardSide.BACK else SingleCardSide.FRONT - when (doAudioReplay) { - true -> cardMediaPlayer.replayAllSounds(side) - false -> cardMediaPlayer.playAllSounds(side) + when (doMediaReplay) { + true -> cardMediaPlayer.replayAll(side) + false -> cardMediaPlayer.playAll(side) } } return @@ -1462,7 +1462,7 @@ abstract class AbstractFlashcardViewer : // Text to speech is in effect here // If the question is displayed or if the question should be replayed, read the question if (ttsInitialized) { - if (!displayAnswer || doAudioReplay && replayQuestion) { + if (!displayAnswer || doMediaReplay && replayQuestion) { readCardTts(SingleCardSide.FRONT) } if (displayAnswer) { @@ -1480,12 +1480,12 @@ abstract class AbstractFlashcardViewer : } /** - * @see CardMediaPlayer.onSoundGroupCompleted + * @see CardMediaPlayer.onMediaGroupCompleted */ - open fun onSoundGroupCompleted() { - Timber.v("onSoundGroupCompleted") + open fun onMediaGroupCompleted() { + Timber.v("onMediaGroupCompleted") launchCatchingTask { - if (automaticAnswerShouldWaitForAudio()) { + if (automaticAnswerShouldWaitForMedia()) { if (isDisplayingAnswer) { automaticAnswer.scheduleAutomaticDisplayQuestion() } else { @@ -1560,7 +1560,7 @@ abstract class AbstractFlashcardViewer : sched.buryCards(listOf(currentCard!!.id)) } } - cardMediaPlayer.stopSounds() + cardMediaPlayer.stop() showSnackbar(R.string.card_buried, Reviewer.ACTION_SNACKBAR_TIME) } return true @@ -1574,7 +1574,7 @@ abstract class AbstractFlashcardViewer : sched.suspendCards(listOf(currentCard!!.id)) } } - cardMediaPlayer.stopSounds() + cardMediaPlayer.stop() showSnackbar(TR.studyingCardSuspended(), Reviewer.ACTION_SNACKBAR_TIME) } return true @@ -1591,7 +1591,7 @@ abstract class AbstractFlashcardViewer : } val count = changed.count val noteSuspended = resources.getQuantityString(R.plurals.note_suspended, count, count) - cardMediaPlayer.stopSounds() + cardMediaPlayer.stop() showSnackbar(noteSuspended, Reviewer.ACTION_SNACKBAR_TIME) } return true @@ -1606,7 +1606,7 @@ abstract class AbstractFlashcardViewer : sched.buryNotes(listOf(currentCard!!.nid)) } } - cardMediaPlayer.stopSounds() + cardMediaPlayer.stop() showSnackbar(TR.studyingCardsBuried(changed.count), Reviewer.ACTION_SNACKBAR_TIME) } return true @@ -1675,7 +1675,7 @@ abstract class AbstractFlashcardViewer : } ViewerCommand.PLAY_MEDIA -> { - playSounds(true) + playMedia(true) true } @@ -2222,7 +2222,7 @@ abstract class AbstractFlashcardViewer : fun ttsInitialized() { ttsInitialized = true if (replayOnTtsInit) { - playSounds(true) + playMedia(true) } } @@ -2367,7 +2367,7 @@ abstract class AbstractFlashcardViewer : fun filterUrl(url: String): Boolean { if (url.startsWith("playsound:")) { launchCatchingTask { - controlSound(url) + controlMedia(url) } return true } @@ -2530,7 +2530,7 @@ abstract class AbstractFlashcardViewer : * @param url */ @NeedsTest("14221: 'playsound' should play the sound from the start") - private suspend fun controlSound(url: String) { + private suspend fun controlMedia(url: String) { val avTag = when (val tag = currentCard?.let { getAvTag(it, url) }) { is SoundOrVideoTag -> tag @@ -2538,7 +2538,7 @@ abstract class AbstractFlashcardViewer : // not currently supported null -> return } - cardMediaPlayer.playOneSound(avTag) + cardMediaPlayer.playOne(avTag) } // Run any post-load events in javascript that rely on the window being completely loaded. diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 0f49e4becbf1..40cccac78097 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -433,8 +433,8 @@ open class Reviewer : onMark(currentCard) } R.id.action_replay -> { - Timber.i("Reviewer:: Replay audio button pressed (from menu)") - playSounds(true) + Timber.i("Reviewer:: Replay media button pressed (from menu)") + playMedia(doMediaReplay = true) } R.id.action_toggle_mic_tool_bar -> { Timber.i("Reviewer:: Voice playback visibility set to %b", !isMicToolBarVisible) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardMediaPlayer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardMediaPlayer.kt index 3012af0a535f..4ff88881838f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardMediaPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardMediaPlayer.kt @@ -30,9 +30,9 @@ import com.ichi2.anki.CollectionHelper.getMediaDirectory import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.R import com.ichi2.anki.ReadText -import com.ichi2.anki.cardviewer.SoundErrorBehavior.CONTINUE_AUDIO -import com.ichi2.anki.cardviewer.SoundErrorBehavior.RETRY_AUDIO -import com.ichi2.anki.cardviewer.SoundErrorBehavior.STOP_AUDIO +import com.ichi2.anki.cardviewer.MediaErrorBehavior.CONTINUE_MEDIA +import com.ichi2.anki.cardviewer.MediaErrorBehavior.RETRY_MEDIA +import com.ichi2.anki.cardviewer.MediaErrorBehavior.STOP_MEDIA import com.ichi2.anki.dialogs.TtsPlaybackErrorDialog import com.ichi2.anki.localizedErrorMessage import com.ichi2.anki.reviewer.CardSide @@ -64,6 +64,7 @@ import java.io.Closeable * * Regular Sound (file-based, mp3 etc..): [SoundOrVideoTag] * * No docs for [sound:], but this handles Sound or Video with a reference to the file * * `[sound:audio.mp3]` in a field + * * `[sound:video.mp4]` in a field * * in the media directory. * * Text to Speech [TTSTag] * * [docs][https://docs.ankiweb.net/templates/fields.html?highlight=tts#text-to-speech] @@ -72,16 +73,16 @@ import java.io.Closeable * This class combines the above concerns behind an "adapter" interface in order to simplify complexity. * * **Public interface** - * * [playAllSounds] - * * [replayAllSounds] - * * [playOneSound] - * * [stopSounds] - * * [loadCardSounds] - informs the class of whether we're on the front/back of a card + * * [playAll] + * * [replayAll] + * * [playOne] + * * [stop] + * * [loadCardAvTags] - informs the class of whether we're on the front/back of a card * * @see AvTag * - * [setOnSoundGroupCompletedListener] can be used to call - * something when [playAllSounds] or [replayAllSounds] completes + * [setOnMediaGroupCompletedListener] can be used to call + * something when [playAll] or [replayAll] completes * * **Out of scope** * [com.ichi2.anki.ReadText]: AnkiDroid has a legacy "tts" setting, before Anki Desktop TTS. @@ -90,17 +91,17 @@ import java.io.Closeable */ @NeedsTest("Integration test: A video is autoplayed if it's the first media on a card") @NeedsTest("A sound is played after a video finishes") -@NeedsTest("Pausing a video calls onSoundGroupCompleted") +@NeedsTest("Pausing a video calls onMediaGroupCompleted") class CardMediaPlayer : Closeable { private val soundTagPlayer: SoundTagPlayer private val ttsPlayer: Deferred - private var soundErrorListener: SoundErrorListener? = null + private var mediaErrorListener: MediaErrorListener? = null var javascriptEvaluator: () -> JavascriptEvaluator? = { null } - constructor(soundTagPlayer: SoundTagPlayer, ttsPlayer: Deferred, soundErrorListener: SoundErrorListener) { + constructor(soundTagPlayer: SoundTagPlayer, ttsPlayer: Deferred, mediaErrorListener: MediaErrorListener) { this.soundTagPlayer = soundTagPlayer this.ttsPlayer = ttsPlayer - this.soundErrorListener = soundErrorListener + this.mediaErrorListener = mediaErrorListener } constructor() { @@ -115,38 +116,38 @@ class CardMediaPlayer : Closeable { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private lateinit var questions: List - private lateinit var answers: List + private lateinit var questionAvTags: List + private lateinit var answerAvTags: List lateinit var config: CardSoundConfig var isEnabled = true set(value) { if (!value) { - scope.launch { stopSounds() } + scope.launch { stop() } } field = value } - private var playSoundsJob: Job? = null - val isPlaying get() = playSoundsJob != null + private var playAvTagsJob: Job? = null + val isPlaying get() = playAvTagsJob != null - private var onSoundGroupCompleted: (() -> Unit)? = null + private var onMediaGroupCompleted: (() -> Unit)? = null - fun setOnSoundGroupCompletedListener(listener: (() -> Unit)?) { - onSoundGroupCompleted = listener + fun setOnMediaGroupCompletedListener(listener: (() -> Unit)?) { + onMediaGroupCompleted = listener } - fun setSoundErrorListener(soundErrorListener: SoundErrorListener) { - this.soundErrorListener = soundErrorListener + fun setMediaErrorListener(mediaErrorListener: MediaErrorListener) { + this.mediaErrorListener = mediaErrorListener } - suspend fun loadCardSounds(card: Card) { - Timber.i("loading sounds for card %d", card.id) - stopSounds() + suspend fun loadCardAvTags(card: Card) { + Timber.i("loading av tags for card %d", card.id) + stop() val renderOutput = withCol { card.renderOutput(this) } val autoPlay = withCol { card.autoplay(this) } - this.questions = renderOutput.questionAvTags - this.answers = renderOutput.answerAvTags + this.questionAvTags = renderOutput.questionAvTags + this.answerAvTags = renderOutput.answerAvTags if (!this::config.isInitialized || !config.appliesTo(card) || (this::config.isInitialized && autoPlay != config.autoplay)) { config = withCol { CardSoundConfig.create(this@withCol, card) } @@ -154,43 +155,43 @@ class CardMediaPlayer : Closeable { } /** - * Ensures that [questions] and [answers] are loaded + * Ensures that [questionAvTags] and [answerAvTags] are loaded * * Does not affect playback if they are */ - suspend fun ensureCardSoundsLoaded(card: Card) { - if (this::questions.isInitialized) return + suspend fun ensureAvTagsLoaded(card: Card) { + if (this::questionAvTags.isInitialized) return Timber.i("loading sounds for card %d", card.id) val renderOutput = withCol { card.renderOutput(this) } - this.questions = renderOutput.questionAvTags - this.answers = renderOutput.answerAvTags + this.questionAvTags = renderOutput.questionAvTags + this.answerAvTags = renderOutput.answerAvTags if (!this::config.isInitialized || !config.appliesTo(card)) { config = withCol { CardSoundConfig.create(this@withCol, card) } } } - suspend fun autoplayAllSoundsForSide(cardSide: CardSide): Job? { + fun autoplayAllForSide(cardSide: CardSide): Job? { if (config.autoplay) { - return playAllSoundsForSide(cardSide) + return playAllForSide(cardSide) } return null } - suspend fun playAllSoundsForSide(cardSide: CardSide): Job? { + fun playAllForSide(cardSide: CardSide): Job? { if (!isEnabled) return null - playSoundsJob { + playAvTagsJob { Timber.i("playing sounds for %s", cardSide) - playAllSoundsInternal(cardSide, isAutomaticPlayback = true) + playAllAvTagsInternal(cardSide, isAutomaticPlayback = true) } - return this.playSoundsJob + return this.playAvTagsJob } - suspend fun playOneSound(tag: AvTag): Job? { + suspend fun playOne(tag: AvTag): Job? { if (!isEnabled) return null - cancelPlaySoundsJob() - Timber.i("playing one sound") + cancelPlayAvTagsJob() + Timber.i("playing one AV Tag") suspend fun play(tag: AvTag) = play(tag, isAutomaticPlayback = false) @@ -200,38 +201,38 @@ class CardMediaPlayer : Closeable { } catch (e: CancellationException) { throw e } catch (e: Exception) { - Timber.w(e, "failed to replay audio") + Timber.w(e, "failed to replay media") } } - playSoundsJob = + playAvTagsJob = scope.launch { try { play(tag) - } catch (e: SoundException) { + } catch (e: MediaException) { when (e.continuationBehavior) { - RETRY_AUDIO -> retry() - CONTINUE_AUDIO, STOP_AUDIO -> { } + RETRY_MEDIA -> retry() + CONTINUE_MEDIA, STOP_MEDIA -> { } } } catch (e: CancellationException) { throw e } catch (e: Exception) { - Timber.w(e, "Exception playing sound") + Timber.w(e, "Exception playing AV Tag") } - Timber.v("completed playing one sound") - playSoundsJob = null + Timber.v("completed playing one AV Tag") + playAvTagsJob = null } - return playSoundsJob + return playAvTagsJob } - suspend fun stopSounds() { - if (isPlaying) Timber.i("stopping sounds") - cancelPlaySoundsJob(playSoundsJob) + suspend fun stop() { + if (isPlaying) Timber.i("stopping playing all AV tags") + cancelPlayAvTagsJob(playAvTagsJob) ReadText.stopTts() // TODO: Reconsider design } override fun close() { - soundTagPlayer.releaseSound() + soundTagPlayer.release() try { ttsPlayer.getCompleted().close() } catch (e: Exception) { @@ -240,44 +241,44 @@ class CardMediaPlayer : Closeable { scope.cancel() } - private suspend fun cancelPlaySoundsJob(job: Job? = playSoundsJob) { + private suspend fun cancelPlayAvTagsJob(job: Job? = playAvTagsJob) { if (job == null) return Timber.i("cancelling job") withContext(Dispatchers.IO) { job.cancelAndJoin() } // This stops multiple calls logging, while allowing an 'old' value in as the parameter - if (job == playSoundsJob) { - playSoundsJob = null + if (job == playAvTagsJob) { + playAvTagsJob = null } } /** - * Obtains all the sounds for the [cardSide] and plays them sequentially + * Obtains all the [AvTag]s for the [cardSide] and plays them sequentially */ - private suspend fun playAllSoundsInternal( + private suspend fun playAllAvTagsInternal( cardSide: CardSide, isAutomaticPlayback: Boolean, ) { if (!isEnabled) return - val soundList = + val avTagList = when (cardSide) { - CardSide.QUESTION -> questions - CardSide.ANSWER -> answers - CardSide.BOTH -> questions + answers + CardSide.QUESTION -> questionAvTags + CardSide.ANSWER -> answerAvTags + CardSide.BOTH -> questionAvTags + answerAvTags } try { - for ((index, sound) in soundList.withIndex()) { - Timber.d("playing sound %d/%d", index + 1, soundList.size) - if (!play(sound, isAutomaticPlayback)) { - Timber.d("stopping sound playback early") + for ((index, avTag) in avTagList.withIndex()) { + Timber.d("playing AV Tag %d/%d", index + 1, avTagList.size) + if (!play(avTag, isAutomaticPlayback)) { + Timber.d("stopping AV Tag playback early") return } } } finally { // call the completion listener, even if a CancellationException was thrown - onSoundGroupCompleted?.invoke() + onMediaGroupCompleted?.invoke() } } @@ -293,10 +294,10 @@ class CardMediaPlayer : Closeable { suspend fun play() { ensureActive() when (tag) { - is SoundOrVideoTag -> soundTagPlayer.play(tag, soundErrorListener) + is SoundOrVideoTag -> soundTagPlayer.play(tag, mediaErrorListener) is TTSTag -> { awaitTtsPlayer(isAutomaticPlayback)?.play(tag)?.error?.let { - soundErrorListener?.onTtsError(it, isAutomaticPlayback) + mediaErrorListener?.onTtsError(it, isAutomaticPlayback) } } } @@ -305,48 +306,49 @@ class CardMediaPlayer : Closeable { try { play() - } catch (e: SoundException) { + } catch (e: MediaException) { when (e.continuationBehavior) { - STOP_AUDIO -> return@withContext false - CONTINUE_AUDIO -> return@withContext true - RETRY_AUDIO -> { + STOP_MEDIA -> return@withContext false + CONTINUE_MEDIA -> return@withContext true + RETRY_MEDIA -> { try { - Timber.i("retrying audio") + Timber.i("retrying media") play() Timber.i("retry succeeded") } catch (e: CancellationException) { throw e } catch (e: Exception) { - Timber.w(e, "retry audio failed") + Timber.w(e, "retry media failed") } } } } catch (e: CancellationException) { throw e } catch (e: Exception) { - Timber.w(e, "Unexpected audio exception. Continuing") + Timber.w(e, "Unexpected media exception. Continuing") } return@withContext true } - fun hasSounds(displayAnswer: Boolean): Boolean = if (displayAnswer) answers.any() else questions.any() + /** Whether the provided side has available media */ + fun hasMedia(displayAnswer: Boolean): Boolean = if (displayAnswer) answerAvTags.any() else questionAvTags.any() /** - * Plays all sounds for the current side, calling [onSoundGroupCompleted] when completed + * Plays all sounds for the current side, calling [onMediaGroupCompleted] when completed */ - suspend fun playAllSounds(side: SingleCardSide) = + fun playAll(side: SingleCardSide) = when (side) { - SingleCardSide.FRONT -> playAllSoundsForSide(CardSide.QUESTION) - SingleCardSide.BACK -> playAllSoundsForSide(CardSide.ANSWER) + SingleCardSide.FRONT -> playAllForSide(CardSide.QUESTION) + SingleCardSide.BACK -> playAllForSide(CardSide.ANSWER) } /** - * Replays all sounds for the current side, calling [onSoundGroupCompleted] when completed + * Replays all sounds for [side], calling [onMediaGroupCompleted] when completed */ - suspend fun replayAllSounds(side: SingleCardSide) = + fun replayAll(side: SingleCardSide) = when (side) { - SingleCardSide.BACK -> if (config.replayQuestion) playAllSoundsForSide(CardSide.BOTH) else playAllSoundsForSide(CardSide.ANSWER) - SingleCardSide.FRONT -> playAllSoundsForSide(CardSide.QUESTION) + SingleCardSide.BACK -> if (config.replayQuestion) playAllForSide(CardSide.BOTH) else playAllForSide(CardSide.ANSWER) + SingleCardSide.FRONT -> playAllForSide(CardSide.QUESTION) } private suspend fun awaitTtsPlayer(isAutomaticPlayback: Boolean): TtsPlayer? { @@ -357,19 +359,19 @@ class CardMediaPlayer : Closeable { if (player == null) { Timber.v("timeout waiting for TTS Player") val error = AndroidTtsError.InitTimeout - soundErrorListener?.onTtsError(error, isAutomaticPlayback) + mediaErrorListener?.onTtsError(error, isAutomaticPlayback) } return player } - /** Ensures that only one [playSoundsJob] is running at once */ - private suspend fun playSoundsJob(block: suspend CoroutineScope.() -> Unit) { - val oldJob = playSoundsJob - this.playSoundsJob = + /** Ensures that only one [playAvTagsJob] is running at once */ + private fun playAvTagsJob(block: suspend CoroutineScope.() -> Unit) { + val oldJob = playAvTagsJob + this.playAvTagsJob = scope.launch { - cancelPlaySoundsJob(oldJob) + cancelPlayAvTagsJob(oldJob) block() - playSoundsJob = null + playAvTagsJob = null } } @@ -388,34 +390,34 @@ class CardMediaPlayer : Closeable { const val TTS_PLAYER_TIMEOUT_MS = 2_500L /** - * @param soundUriBase The base path to the sound directory as a `file://` URI + * @param mediaUriBase The base path to the media directory as a `file://` URI */ @NeedsTest("ensure the lifecycle is subscribed to in a Reviewer") fun newInstance( viewer: AbstractFlashcardViewer, - soundUriBase: String, + mediaUriBase: String, ): CardMediaPlayer { val scope = viewer.lifecycleScope - val soundErrorListener = viewer.createSoundErrorListener() + val soundErrorListener = viewer.createMediaErrorListener() // tts can take a long time to init, this defers the operation until it's needed val tts = scope.async(Dispatchers.IO) { AndroidTtsPlayer.createInstance(viewer.lifecycleScope) } - val soundPlayer = SoundTagPlayer(soundUriBase, VideoPlayer { viewer.webViewClient!! }) + val soundPlayer = SoundTagPlayer(mediaUriBase, VideoPlayer { viewer.webViewClient!! }) return CardMediaPlayer( soundTagPlayer = soundPlayer, ttsPlayer = tts, - soundErrorListener = soundErrorListener, + mediaErrorListener = soundErrorListener, ).apply { - setOnSoundGroupCompletedListener(viewer::onSoundGroupCompleted) + setOnMediaGroupCompletedListener(viewer::onMediaGroupCompleted) } } } } -interface SoundErrorListener { +interface MediaErrorListener { @CheckResult - fun onError(uri: Uri): SoundErrorBehavior + fun onError(uri: Uri): MediaErrorBehavior @CheckResult fun onMediaPlayerError( @@ -423,7 +425,7 @@ interface SoundErrorListener { which: Int, extra: Int, uri: Uri, - ): SoundErrorBehavior + ): MediaErrorBehavior fun onTtsError( error: TtsPlayer.TtsError, @@ -431,28 +433,26 @@ interface SoundErrorListener { ) } -enum class SoundErrorBehavior { - /** Stop playing audio */ - STOP_AUDIO, +enum class MediaErrorBehavior { + /** Stop playing media */ + STOP_MEDIA, - /** Continue to the next audio (if any) */ - CONTINUE_AUDIO, + /** Continue to the next media (if any) */ + CONTINUE_MEDIA, - /** Retry the current audio */ - RETRY_AUDIO, + /** Retry the current media */ + RETRY_MEDIA, } -fun AbstractFlashcardViewer.createSoundErrorListener(): SoundErrorListener { +fun AbstractFlashcardViewer.createMediaErrorListener(): MediaErrorListener { val activity = this - return object : SoundErrorListener { - private var handledError: HashSet = hashSetOf() - + return object : MediaErrorListener { override fun onMediaPlayerError( mp: MediaPlayer?, which: Int, extra: Int, uri: Uri, - ): SoundErrorBehavior { + ): MediaErrorBehavior { Timber.w("Media Error: (%d, %d)", which, extra) return onError(uri) } @@ -472,36 +472,36 @@ fun AbstractFlashcardViewer.createSoundErrorListener(): SoundErrorListener { } } - override fun onError(uri: Uri): SoundErrorBehavior { + override fun onError(uri: Uri): MediaErrorBehavior { if (uri.scheme != "file") { - return CONTINUE_AUDIO + return CONTINUE_MEDIA } try { val file = uri.toFile() // There is a multitude of transient issues with the MediaPlayer. (1, -1001) for example // Retrying fixes most of these - if (file.exists()) return RETRY_AUDIO + if (file.exists()) return RETRY_MEDIA // just doesn't exist - process the error - AbstractFlashcardViewer.mediaErrorHandler.processMissingSound( + AbstractFlashcardViewer.mediaErrorHandler.processMissingMedia( file, ) { filename: String? -> displayCouldNotFindMediaSnackbar(filename) } - return CONTINUE_AUDIO + return CONTINUE_MEDIA } catch (e: Exception) { Timber.w(e) - return CONTINUE_AUDIO + return CONTINUE_MEDIA } } } } /** An exception thrown when playing a sound, and how to continue playing sounds */ -class SoundException : Exception { - val continuationBehavior: SoundErrorBehavior - constructor(errorHandling: SoundErrorBehavior) : super() { +class MediaException : Exception { + val continuationBehavior: MediaErrorBehavior + constructor(errorHandling: MediaErrorBehavior) : super() { this.continuationBehavior = errorHandling } - constructor(errorHandling: SoundErrorBehavior, exception: Exception) : super(exception) { + constructor(errorHandling: MediaErrorBehavior, exception: Exception) : super(exception) { this.continuationBehavior = errorHandling } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/MediaErrorHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/MediaErrorHandler.kt index 966b365c1c41..e9a3c08cc27a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/MediaErrorHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/MediaErrorHandler.kt @@ -61,7 +61,7 @@ class MediaErrorHandler { } } - fun processMissingSound( + fun processMissingMedia( file: File, onFailure: (String) -> Unit, ) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundTagPlayer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundTagPlayer.kt index f46273facfba..66b0a689c3eb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundTagPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/SoundTagPlayer.kt @@ -37,7 +37,7 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -/** Player for the sounds of [SoundOrVideoTag] */ +/** Player for (`[sound:...]`): [SoundOrVideoTag] */ @NeedsTest("CardSoundConfig.autoplay should mean that video also isn't played automatically") class SoundTagPlayer( private val soundUriBase: String, @@ -66,18 +66,18 @@ class SoundTagPlayer( } /** - * @throws SoundException if the file does not exist, or if media playing fails - * @param soundErrorListener handles a sound error and returns how to continue playing sounds + * @throws MediaException if the file does not exist, or if media playing fails + * @param mediaErrorListener handles a sound error and returns how to continue playing sounds */ suspend fun play( tag: SoundOrVideoTag, - soundErrorListener: SoundErrorListener?, + mediaErrorListener: MediaErrorListener?, ) { val tagType = tag.getType() return suspendCancellableCoroutine { continuation -> Timber.d("Playing SoundOrVideoTag") when (tagType) { - SoundOrVideoTag.Type.AUDIO -> playSound(continuation, tag, soundErrorListener) + SoundOrVideoTag.Type.AUDIO -> playSound(continuation, tag, mediaErrorListener) SoundOrVideoTag.Type.VIDEO -> playVideo(continuation, tag) } } @@ -95,12 +95,12 @@ class SoundTagPlayer( private fun playSound( continuation: CancellableContinuation, tag: SoundOrVideoTag, - soundErrorListener: SoundErrorListener?, + mediaErrorListener: MediaErrorListener?, ) { requireNewMediaPlayer().apply { continuation.invokeOnCancellation { Timber.i("stopping MediaPlayer due to cancellation") - stopSounds() + this@SoundTagPlayer.stop() } setOnCompletionListener { Timber.v("finished playing SoundOrVideoTag successfully") @@ -122,10 +122,10 @@ class SoundTagPlayer( Timber.w("Media error %d", what) abandonAudioFocus() val continuationBehavior = - soundErrorListener?.onMediaPlayerError(mp, what, extra, soundUri) ?: SoundErrorBehavior.CONTINUE_AUDIO + mediaErrorListener?.onMediaPlayerError(mp, what, extra, soundUri) ?: MediaErrorBehavior.CONTINUE_MEDIA // 15103: setOnErrorListener can be invoked after task cancellation if (!continuation.isCompleted) { - continuation.resumeWithException(SoundException(continuationBehavior)) + continuation.resumeWithException(MediaException(continuationBehavior)) } true // do not call onCompletionListen } @@ -134,8 +134,8 @@ class SoundTagPlayer( awaitSetDataSource(soundUri.toString()) } catch (e: Exception) { continuation.ensureActive() - val continuationBehavior = soundErrorListener?.onError(soundUri) ?: SoundErrorBehavior.CONTINUE_AUDIO - val exception = SoundException(continuationBehavior, e) + val continuationBehavior = mediaErrorListener?.onError(soundUri) ?: MediaErrorBehavior.CONTINUE_MEDIA + val exception = MediaException(continuationBehavior, e) return continuation.resumeWithException(exception) } @@ -151,9 +151,9 @@ class SoundTagPlayer( } /** - * Releases the sound. + * Releases the media players. */ - fun releaseSound() { + fun release() { Timber.d("Releasing sounds and abandoning audio focus") mediaPlayer?.let { // Required to remove warning: "mediaplayer went away with unhandled events" @@ -165,7 +165,7 @@ class SoundTagPlayer( abandonAudioFocus() } - fun stopSounds() { + fun stop() { try { mediaPlayer?.stop() } catch (e: Exception) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/VideoPlayer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/VideoPlayer.kt index 7b79ed6893a5..6059b6a8bec9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/VideoPlayer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/VideoPlayer.kt @@ -75,7 +75,7 @@ class VideoPlayer( fun onVideoPaused() { Timber.i("video paused") - continuation?.resumeWithException(SoundException(SoundErrorBehavior.STOP_AUDIO)) + continuation?.resumeWithException(MediaException(MediaErrorBehavior.STOP_MEDIA)) continuation = null } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerViewModel.kt index 85d21cdff045..a41530dfb794 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerViewModel.kt @@ -25,9 +25,9 @@ import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.OnErrorListener import com.ichi2.anki.cardviewer.CardMediaPlayer import com.ichi2.anki.cardviewer.JavascriptEvaluator +import com.ichi2.anki.cardviewer.MediaErrorBehavior import com.ichi2.anki.cardviewer.MediaErrorHandler -import com.ichi2.anki.cardviewer.SoundErrorBehavior -import com.ichi2.anki.cardviewer.SoundErrorListener +import com.ichi2.anki.cardviewer.MediaErrorListener import com.ichi2.anki.launchCatchingIO import com.ichi2.anki.pages.AnkiServer import com.ichi2.anki.pages.PostRequestHandler @@ -38,7 +38,6 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber @@ -58,7 +57,7 @@ abstract class CardViewerViewModel( protected val cardMediaPlayer = cardMediaPlayer.apply { - setSoundErrorListener(createSoundErrorListener()) + setMediaErrorListener(createSoundErrorListener()) javascriptEvaluator = { JavascriptEvaluator { launchCatchingIO { eval.emit(it) } } } } abstract var currentCard: Deferred @@ -92,7 +91,7 @@ abstract class CardViewerViewModel( fun playSoundFromUrl(url: String) { launchCatchingIO { Sound.getAvTag(currentCard.await(), url)?.let { - cardMediaPlayer.playOneSound(it) + cardMediaPlayer.playOne(it) } } } @@ -158,21 +157,21 @@ abstract class CardViewerViewModel( eval.emit("_showAnswer(${Json.encodeToString(answer)}, '${bodyClass()}');") } - private fun createSoundErrorListener(): SoundErrorListener { - return object : SoundErrorListener { - override fun onError(uri: Uri): SoundErrorBehavior { + private fun createSoundErrorListener(): MediaErrorListener { + return object : MediaErrorListener { + override fun onError(uri: Uri): MediaErrorBehavior { if (uri.scheme != "file") { - return SoundErrorBehavior.CONTINUE_AUDIO + return MediaErrorBehavior.CONTINUE_MEDIA } val file = uri.toFile() // There is a multitude of transient issues with the MediaPlayer. // Retrying fixes most of these - if (file.exists()) return SoundErrorBehavior.RETRY_AUDIO - mediaErrorHandler.processMissingSound(file) { fileName -> + if (file.exists()) return MediaErrorBehavior.RETRY_MEDIA + mediaErrorHandler.processMissingMedia(file) { fileName -> viewModelScope.launch { onMediaError.emit(fileName) } } - return SoundErrorBehavior.CONTINUE_AUDIO + return MediaErrorBehavior.CONTINUE_MEDIA } override fun onMediaPlayerError( @@ -180,7 +179,7 @@ abstract class CardViewerViewModel( which: Int, extra: Int, uri: Uri, - ): SoundErrorBehavior { + ): MediaErrorBehavior { Timber.w("Media Error: (%d, %d)", which, extra) return onError(uri) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerAction.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerAction.kt index 20b43d1b2988..6d5ef4679fd8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerAction.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerAction.kt @@ -29,6 +29,8 @@ enum class PreviewerAction : MappableAction { MARK, EDIT, TOGGLE_BACKSIDE_ONLY, + + // TODO: rename to REPLAY_MEDIA and handle previewer_replay_audio_key REPLAY_AUDIO, TOGGLE_FLAG_RED, TOGGLE_FLAG_ORANGE, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt index e3ee36e6de1d..b665a70471e6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt @@ -39,6 +39,7 @@ import com.ichi2.anki.DispatchKeyEventListener import com.ichi2.anki.Flag import com.ichi2.anki.R import com.ichi2.anki.browser.IdsFile +import com.ichi2.anki.previewer.PreviewerFragment.Companion.CARD_IDS_FILE_ARG import com.ichi2.anki.reviewer.BindingMap import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.MappableBinding @@ -229,7 +230,7 @@ class PreviewerFragment : PreviewerAction.MARK -> viewModel.toggleMark() PreviewerAction.EDIT -> editCard() PreviewerAction.TOGGLE_BACKSIDE_ONLY -> viewModel.toggleBackSideOnly() - PreviewerAction.REPLAY_AUDIO -> viewModel.replayAudios() + PreviewerAction.REPLAY_AUDIO -> viewModel.replayMedia() PreviewerAction.TOGGLE_FLAG_RED -> viewModel.toggleFlag(Flag.RED) PreviewerAction.TOGGLE_FLAG_ORANGE -> viewModel.toggleFlag(Flag.ORANGE) PreviewerAction.TOGGLE_FLAG_GREEN -> viewModel.toggleFlag(Flag.GREEN) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt index e14f9510701c..5d4b24c4c6af 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt @@ -99,7 +99,7 @@ class PreviewerViewModel( // * after recreation (ViewModel did not exist) // if the ViewModel existed, we want to continue playing audio // if not, we want to setup the sound player - cardMediaPlayer.ensureCardSoundsLoaded(currentCard.await()) + cardMediaPlayer.ensureAvTagsLoaded(currentCard.await()) } return } @@ -117,10 +117,10 @@ class PreviewerViewModel( backSideOnly.emit(!backSideOnly.value) if (!backSideOnly.value && showingAnswer.value) { showQuestion() - cardMediaPlayer.autoplayAllSoundsForSide(CardSide.QUESTION) + cardMediaPlayer.autoplayAllForSide(CardSide.QUESTION) } else if (backSideOnly.value && !showingAnswer.value) { showAnswer() - cardMediaPlayer.autoplayAllSoundsForSide(CardSide.ANSWER) + cardMediaPlayer.autoplayAllForSide(CardSide.ANSWER) } } } @@ -160,7 +160,7 @@ class PreviewerViewModel( launchCatchingIO { if (!showingAnswer.value && !backSideOnly.value) { showAnswer() - cardMediaPlayer.autoplayAllSoundsForSide(CardSide.ANSWER) + cardMediaPlayer.autoplayAllForSide(CardSide.ANSWER) } else { currentIndex.update { it + 1 } } @@ -183,10 +183,10 @@ class PreviewerViewModel( suspend fun getNoteEditorDestination() = NoteEditorLauncher.EditNoteFromPreviewer(currentCard.await().id) - fun replayAudios() { + fun replayMedia() { launchCatchingIO { val side = if (showingAnswer.value) SingleCardSide.BACK else SingleCardSide.FRONT - cardMediaPlayer.replayAllSounds(side) + cardMediaPlayer.replayAll(side) } } @@ -229,8 +229,8 @@ class PreviewerViewModel( showingAnswer.value -> CardSide.ANSWER else -> CardSide.QUESTION } - cardMediaPlayer.loadCardSounds(currentCard.await()) - cardMediaPlayer.autoplayAllSoundsForSide(side) + cardMediaPlayer.loadCardAvTags(currentCard.await()) + cardMediaPlayer.autoplayAllForSide(side) } /** From the [desktop code](https://github.com/ankitects/anki/blob/1ff55475b93ac43748d513794bcaabd5d7df6d9d/qt/aqt/reviewer.py#L671) */ diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/TemplatePreviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/TemplatePreviewerViewModel.kt index 9fc021fbe72c..29d1c4664dcc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/TemplatePreviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/TemplatePreviewerViewModel.kt @@ -208,8 +208,8 @@ class TemplatePreviewerViewModel( ********************************************************************************************* */ private suspend fun loadAndPlaySounds(side: CardSide) { - cardMediaPlayer.loadCardSounds(currentCard.await()) - cardMediaPlayer.autoplayAllSoundsForSide(side) + cardMediaPlayer.loadCardAvTags(currentCard.await()) + cardMediaPlayer.autoplayAllForSide(side) } // https://github.com/ankitects/anki/blob/df70564079f53e587dc44f015c503fdf6a70924f/qt/aqt/clayout.py#L579 diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt index 06778ea3e64c..60f8f4dba1af 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerViewModel.kt @@ -141,7 +141,7 @@ class ReviewerViewModel( // button with the answer buttons. updateNextTimes() } - cardMediaPlayer.setOnSoundGroupCompletedListener { + cardMediaPlayer.setOnMediaGroupCompletedListener { launchCatchingIO { if (!autoAdvance.shouldWaitForAudio()) return@launchCatchingIO @@ -182,10 +182,10 @@ class ReviewerViewModel( } updateNextTimes() showAnswer(typedAnswer) - loadAndPlaySounds(CardSide.ANSWER) + loadAndPlayMedia(CardSide.ANSWER) if (!autoAdvance.shouldWaitForAudio()) { autoAdvance.onShowAnswer() - } // else wait for onSoundGroupCompleted + } // else wait for onMediaGroupCompleted } } @@ -395,7 +395,7 @@ class ReviewerViewModel( runStateMutationHook() if (!autoAdvance.shouldWaitForAudio()) { autoAdvance.onShowQuestion() - } // else run in onSoundGroupCompleted + } // else run in onMediaGroupCompleted } private suspend fun runStateMutationHook() { @@ -448,10 +448,10 @@ class ReviewerViewModel( } } - private suspend fun loadAndPlaySounds(side: CardSide) { + private suspend fun loadAndPlayMedia(side: CardSide) { Timber.v("ReviewerViewModel::loadAndPlaySounds") - cardMediaPlayer.loadCardSounds(currentCard.await()) - cardMediaPlayer.playAllSoundsForSide(side) + cardMediaPlayer.loadCardAvTags(currentCard.await()) + cardMediaPlayer.playAllForSide(side) } private suspend fun updateMarkIcon() { @@ -485,7 +485,7 @@ class ReviewerViewModel( currentCard = CompletableDeferred(card) autoAdvance.onCardChange(card) showQuestion() - loadAndPlaySounds(CardSide.QUESTION) + loadAndPlayMedia(CardSide.QUESTION) updateMarkIcon() updateFlagIcon() canBuryNoteFlow.emit(isBuryNoteAvailable(card)) diff --git a/AnkiDroid/src/main/res/menu/reviewer.xml b/AnkiDroid/src/main/res/menu/reviewer.xml index dff569ffb447..b64b8e8b335f 100644 --- a/AnkiDroid/src/main/res/menu/reviewer.xml +++ b/AnkiDroid/src/main/res/menu/reviewer.xml @@ -174,7 +174,7 @@ Show whiteboard Hide whiteboard Clear whiteboard - Replay audio + Replay media Card marked as leech and suspended Card marked as leech Unknown error diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index 68b15e9296f2..a1bf8f8a3589 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -144,7 +144,7 @@ previewer_BACK previewer_MARK previewer_EDIT - previewer_REPLAY_AUDIO + previewer_REPLAY_AUDIO previewer_BACKSIDE_ONLY previewer_TOGGLE_FLAG_RED previewer_TOGGLE_FLAG_ORANGE diff --git a/AnkiDroid/src/main/res/xml/preferences_custom_buttons.xml b/AnkiDroid/src/main/res/xml/preferences_custom_buttons.xml index dbb52fcb7508..63f199b45b98 100644 --- a/AnkiDroid/src/main/res/xml/preferences_custom_buttons.xml +++ b/AnkiDroid/src/main/res/xml/preferences_custom_buttons.xml @@ -87,7 +87,7 @@ TODO: Add a unit test android:entries="@array/custom_button_labels" android:entryValues="@array/custom_button_values" android:key="@string/custom_button_replay_key" - android:title="@string/replay_audio" + android:title="@string/replay_media" app:useSimpleSummaryProvider="true"/> () @@ -420,8 +420,8 @@ class ReviewerKeyboardInputTest : RobolectricTest() { return true } - override fun playSounds(doAudioReplay: Boolean) { - replayAudioCalled = true + override fun playMedia(doMediaReplay: Boolean) { + replayMediaCalled = true } override fun buryNote(): Boolean { diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardMediaPlayerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardMediaPlayerTest.kt index e507945b7a11..b1fdbc479ceb 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardMediaPlayerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/CardMediaPlayerTest.kt @@ -18,10 +18,10 @@ package com.ichi2.anki.cardviewer import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.CardUtils +import com.ichi2.anki.cardviewer.MediaErrorBehavior.CONTINUE_MEDIA +import com.ichi2.anki.cardviewer.MediaErrorBehavior.RETRY_MEDIA +import com.ichi2.anki.cardviewer.MediaErrorBehavior.STOP_MEDIA import com.ichi2.anki.cardviewer.SingleCardSide.BACK -import com.ichi2.anki.cardviewer.SoundErrorBehavior.CONTINUE_AUDIO -import com.ichi2.anki.cardviewer.SoundErrorBehavior.RETRY_AUDIO -import com.ichi2.anki.cardviewer.SoundErrorBehavior.STOP_AUDIO import com.ichi2.libanki.AvTag import com.ichi2.libanki.SoundOrVideoTag import com.ichi2.libanki.TemplateManager @@ -45,7 +45,7 @@ import org.junit.runner.RunWith class CardMediaPlayerTest : JvmTest() { internal val tagPlayer: SoundTagPlayer = mockk() internal val ttsPlayer: TtsPlayer = mockk() - internal val onSoundGroupCompleted: () -> Unit = + internal val onMediaGroupCompleted: () -> Unit = mockk<() -> Unit>().also { every { it.invoke() } answers { } } @@ -56,7 +56,7 @@ class CardMediaPlayerTest : JvmTest() { answers = emptyList(), questions = emptyList(), ) { - playAllSoundsAndWait(BACK) + playAllAndWait(BACK) verifyNoSoundsPlayed() } @@ -66,11 +66,11 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( questions = listOf(SoundOrVideoTag("abc.mp3")), ) { - playAllSoundsAndWait() + playAllAndWait() coVerify(exactly = 1) { tagPlayer.play(SoundOrVideoTag("abc.mp3"), any()) } coVerify(exactly = 0) { ttsPlayer.play(any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } @Test @@ -78,7 +78,7 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( answers = listOf(SoundOrVideoTag("abc.mp3")), ) { - playAllSoundsAndWait() + playAllAndWait() verifyNoSoundsPlayed() } @@ -88,7 +88,7 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( questions = listOf(SoundOrVideoTag("abc.mp3")), ) { - playAllSoundsAndWait(BACK) + playAllAndWait(BACK) verifyNoSoundsPlayed() } @@ -100,7 +100,7 @@ class CardMediaPlayerTest : JvmTest() { answers = listOf(SoundOrVideoTag("back.mp3")), replayQuestion = true, ) { - replayAllSoundsAndWait(BACK) + replayAllAndWait(BACK) coVerifyOrder { tagPlayer.play(SoundOrVideoTag("front.mp3"), any()) @@ -115,7 +115,7 @@ class CardMediaPlayerTest : JvmTest() { answers = listOf(SoundOrVideoTag("back.mp3")), replayQuestion = false, ) { - replayAllSoundsAndWait(BACK) + replayAllAndWait(BACK) coVerifyOrder { tagPlayer.play(SoundOrVideoTag("back.mp3"), any()) @@ -123,16 +123,16 @@ class CardMediaPlayerTest : JvmTest() { } @Test - fun `onSoundGroupCompleted is called after exception`() = + fun `onMediaGroupCompleted is called after exception`() = runSoundPlayerTest( questions = listOf(SoundOrVideoTag("aa.mp3")), ) { coEvery { tagPlayer.play(any(), any()) } throws TestException("test") - playAllSoundsAndWait() + playAllAndWait() coVerify(exactly = 1) { tagPlayer.play(any(), any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } @Test @@ -140,9 +140,9 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( questions = listOf(SoundOrVideoTag("aa.mp3"), SoundOrVideoTag("bb.mp3")), ) { - coEvery { tagPlayer.play(any(), any()) } throws SoundException(RETRY_AUDIO) + coEvery { tagPlayer.play(any(), any()) } throws MediaException(RETRY_MEDIA) - playAllSoundsAndWait() + playAllAndWait() coVerifySequence { tagPlayer.play(SoundOrVideoTag("aa.mp3"), any()) @@ -151,7 +151,7 @@ class CardMediaPlayerTest : JvmTest() { tagPlayer.play(SoundOrVideoTag("bb.mp3"), any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } @Test @@ -159,15 +159,15 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( questions = listOf(SoundOrVideoTag("aa.mp3"), SoundOrVideoTag("bb.mp3")), ) { - coEvery { tagPlayer.play(any(), any()) } throws SoundException(STOP_AUDIO) + coEvery { tagPlayer.play(any(), any()) } throws MediaException(STOP_MEDIA) - playAllSoundsAndWait() + playAllAndWait() coVerifySequence { tagPlayer.play(SoundOrVideoTag("aa.mp3"), any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } @Test @@ -175,24 +175,24 @@ class CardMediaPlayerTest : JvmTest() { runSoundPlayerTest( questions = listOf(SoundOrVideoTag("aa.mp3"), SoundOrVideoTag("bb.mp3")), ) { - coEvery { tagPlayer.play(any(), any()) } throws SoundException(CONTINUE_AUDIO) + coEvery { tagPlayer.play(any(), any()) } throws MediaException(CONTINUE_MEDIA) - playAllSoundsAndWait() + playAllAndWait() coVerifySequence { tagPlayer.play(SoundOrVideoTag("aa.mp3"), any()) tagPlayer.play(SoundOrVideoTag("bb.mp3"), any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } @Test fun `retry playing single sound`() = runSoundPlayerTest { - coEvery { tagPlayer.play(any(), any()) } throws SoundException(RETRY_AUDIO) + coEvery { tagPlayer.play(any(), any()) } throws MediaException(RETRY_MEDIA) - playOneSoundAndWait(SoundOrVideoTag("a.mp3")) + playOneAndWait(SoundOrVideoTag("a.mp3")) coVerifySequence { tagPlayer.play(SoundOrVideoTag("a.mp3"), any()) @@ -203,23 +203,23 @@ class CardMediaPlayerTest : JvmTest() { private fun verifyNoSoundsPlayed() { coVerify(exactly = 0) { tagPlayer.play(any(), any()) } coVerify(exactly = 0) { ttsPlayer.play(any()) } - ensureOnSoundGroupCompletedCalled() + ensureOnMediaGroupCompletedCalled() } - private fun ensureOnSoundGroupCompletedCalled() { - verify(exactly = 1) { onSoundGroupCompleted.invoke() } + private fun ensureOnMediaGroupCompletedCalled() { + verify(exactly = 1) { onMediaGroupCompleted.invoke() } } - private suspend fun CardMediaPlayer.playAllSoundsAndWait(side: SingleCardSide = SingleCardSide.FRONT) { - this.playAllSounds(side)?.join() + private suspend fun CardMediaPlayer.playAllAndWait(side: SingleCardSide = SingleCardSide.FRONT) { + this.playAll(side)?.join() } - private suspend fun CardMediaPlayer.replayAllSoundsAndWait(side: SingleCardSide) { - this.replayAllSounds(side)?.join() + private suspend fun CardMediaPlayer.replayAllAndWait(side: SingleCardSide) { + this.replayAll(side)?.join() } - private suspend fun CardMediaPlayer.playOneSoundAndWait(tag: AvTag) { - playOneSound(tag)?.join() + private suspend fun CardMediaPlayer.playOneAndWait(tag: AvTag) { + playOne(tag)?.join() } suspend fun CardMediaPlayer.setup( @@ -252,7 +252,7 @@ class CardMediaPlayerTest : JvmTest() { } } - this.loadCardSounds(card) + this.loadCardAvTags(card) } } @@ -272,9 +272,9 @@ fun CardMediaPlayerTest.runSoundPlayerTest( CardMediaPlayer( soundTagPlayer = tagPlayer, ttsPlayer = CompletableDeferred(ttsPlayer), - soundErrorListener = mockk(), + mediaErrorListener = mockk(), ) - cardMediaPlayer.setOnSoundGroupCompletedListener(onSoundGroupCompleted) + cardMediaPlayer.setOnMediaGroupCompletedListener(onMediaGroupCompleted) assertThat("can play sounds", cardMediaPlayer.isEnabled) cardMediaPlayer.setup(questions, answers, replayQuestion, autoplay) testBody(cardMediaPlayer) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/MediaErrorHandlerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/MediaErrorHandlerTest.kt index 4820a500b98a..ecc7a1d4bd03 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/MediaErrorHandlerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/MediaErrorHandlerTest.kt @@ -102,11 +102,11 @@ class MediaErrorHandlerTest { sut.processFailure(invalidRequest, consumer) } - private fun processMissingSound( + private fun processMissingMedia( file: File, onFailure: (String) -> Unit, ) { - sut.processMissingSound(file, onFailure) + sut.processMissingMedia(file, onFailure) } @Test @@ -119,18 +119,18 @@ class MediaErrorHandlerTest { fun testThirdSoundIsIgnored() { // Tests that the third call to processMissingSound is ignored val handler = defaultHandler() - processMissingSound(File("example.wav"), handler) + processMissingMedia(File("example.wav"), handler) sut.onCardSideChange() - processMissingSound(File("example2.wav"), handler) + processMissingMedia(File("example2.wav"), handler) sut.onCardSideChange() - processMissingSound(File("example3.wav"), handler) + processMissingMedia(File("example3.wav"), handler) assertThat(timesCalled, equalTo(2)) assertThat(fileNames, contains("example.wav", "example2.wav")) } @Test fun testMissingSound_ExceptionCaught() { - assertDoesNotThrow { processMissingSound(File("example.wav")) { throw RuntimeException("expected") } } + assertDoesNotThrow { processMissingMedia(File("example.wav")) { throw RuntimeException("expected") } } } private fun getValidRequest(fileName: String): WebResourceRequest { diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/VideoPlayerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/VideoPlayerTest.kt index 96eee4fdd34f..10cc04e10fa9 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/VideoPlayerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/cardviewer/VideoPlayerTest.kt @@ -47,8 +47,8 @@ class VideoPlayerTest : RobolectricTest() { val result = assertNotNull(m.result) assertThat("failure", result.isFailure) - val exception = result.exceptionOrNull() as? SoundException - assertThat("Audio is stopped", exception != null && exception.continuationBehavior == SoundErrorBehavior.STOP_AUDIO) + val exception = result.exceptionOrNull() as? MediaException + assertThat("Audio is stopped", exception != null && exception.continuationBehavior == MediaErrorBehavior.STOP_MEDIA) } // TODO: use a mock - couldn't get mockk working here