-
Notifications
You must be signed in to change notification settings - Fork 116
Implement a new TTS navigator #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
648d1c2
92cafbf
0de1cfc
de80567
92522e4
b8379af
c785aa3
4150a47
aff1ffc
60901b1
e1e0363
98786bd
e07b459
362451b
fde1cbc
b14405a
c76def0
141efe6
23000c6
64b52a2
25a69d9
7699637
5c1ff6c
da0a3ec
dcc8841
1672154
c6df6e6
940fc6c
1da03ec
60213fb
62a57db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| name: Checks | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: [ main, develop ] | ||
| pull_request: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import androidx.media3.common.MediaMetadata | ||
| import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Deferred | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.async | ||
| import org.readium.r2.shared.publication.Publication | ||
|
|
||
| /** | ||
| * Builds media metadata using the given title, author and cover, | ||
| * and fall back on what is in the publication. | ||
| */ | ||
| @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) | ||
| internal class DefaultMediaMetadataFactory( | ||
| private val publication: Publication, | ||
| title: String? = null, | ||
| author: String? = null, | ||
| cover: ByteArray? = null | ||
| ) : MediaMetadataFactory { | ||
|
|
||
| private val coroutineScope = | ||
| CoroutineScope(Dispatchers.Default) | ||
|
|
||
| private val title: String = | ||
| title ?: publication.metadata.title | ||
|
|
||
| private val authors: String? = | ||
| author ?: publication.metadata.authors | ||
| .firstOrNull { it.name.isNotBlank() }?.name | ||
|
|
||
| private val cover: Deferred<ByteArray?> = coroutineScope.async { | ||
| cover ?: publication.linkWithRel("cover") | ||
| ?.let { publication.get(it) } | ||
| ?.read() | ||
| ?.getOrNull() | ||
| } | ||
|
|
||
| override suspend fun publicationMetadata(): MediaMetadata { | ||
| val builder = MediaMetadata.Builder() | ||
| .setTitle(title) | ||
| .setTotalTrackCount(publication.readingOrder.size) | ||
|
|
||
| authors | ||
| ?.let { builder.setArtist(it) } | ||
|
|
||
| cover.await() | ||
| ?.let { builder.maybeSetArtworkData(it, PICTURE_TYPE_FRONT_COVER) } | ||
|
|
||
| return builder.build() | ||
| } | ||
|
|
||
| override suspend fun resourceMetadata(index: Int): MediaMetadata { | ||
| val builder = MediaMetadata.Builder() | ||
| .setTrackNumber(index) | ||
| .setTitle(title) | ||
|
|
||
| authors | ||
| ?.let { builder.setArtist(it) } | ||
|
|
||
| cover.await() | ||
| ?.let { builder.maybeSetArtworkData(it, PICTURE_TYPE_FRONT_COVER) } | ||
|
|
||
| return builder.build() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import org.readium.r2.shared.publication.Publication | ||
|
|
||
| /** | ||
| * Builds a [MediaMetadataFactory] which will use the given title, author and cover, | ||
| * and fall back on what is in the publication. | ||
| */ | ||
| class DefaultMediaMetadataProvider( | ||
| private val title: String? = null, | ||
| private val author: String? = null, | ||
| private val cover: ByteArray? = null | ||
| ) : MediaMetadataProvider { | ||
|
|
||
| override fun createMetadataFactory(publication: Publication): MediaMetadataFactory { | ||
| return DefaultMediaMetadataFactory(publication, title, author, cover) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import androidx.media3.common.MediaMetadata | ||
|
|
||
| /** | ||
| * Factory for the [MediaMetadata] associated with the publication and its resources. | ||
| * | ||
| * The metadata are used for example in the media-style Android notification. | ||
| */ | ||
| interface MediaMetadataFactory { | ||
|
|
||
| /** | ||
| * Creates the [MediaMetadata] for the whole publication. | ||
| */ | ||
| suspend fun publicationMetadata(): MediaMetadata | ||
|
|
||
| /** | ||
| * Creates the [MediaMetadata] for the reading order resource at the given [index]. | ||
| */ | ||
| suspend fun resourceMetadata(index: Int): MediaMetadata | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import org.readium.r2.shared.publication.Publication | ||
|
|
||
| /** | ||
| * To be implemented to use a custom [MediaMetadataFactory]. | ||
| */ | ||
| fun interface MediaMetadataProvider { | ||
|
|
||
| fun createMetadataFactory(publication: Publication): MediaMetadataFactory | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import androidx.media3.common.Player | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Media controls don't seem to be working (e.g. from headphones)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not true! The issue is that any other app takes precedence. I've not been able to figure out the reason so far. |
||
| import kotlinx.coroutines.flow.StateFlow | ||
| import org.readium.r2.navigator.Navigator | ||
| import org.readium.r2.shared.ExperimentalReadiumApi | ||
| import org.readium.r2.shared.util.Closeable | ||
|
|
||
| @ExperimentalReadiumApi | ||
| interface MediaNavigator<P : MediaNavigator.Position> : Navigator, Closeable { | ||
|
|
||
| /** | ||
| * Marker interface for the [position] flow. | ||
| */ | ||
| interface Position | ||
|
|
||
| /** | ||
| * State of the player. | ||
| */ | ||
| sealed interface State { | ||
|
|
||
| /** | ||
| * The navigator is ready to play. | ||
| */ | ||
| interface Ready : State | ||
|
|
||
| /** | ||
| * The end of the media has been reached. | ||
| */ | ||
| interface Ended : State | ||
|
|
||
| /** | ||
| * The navigator cannot play because the buffer is starved. | ||
| */ | ||
| interface Buffering : State | ||
|
|
||
| /** | ||
| * The navigator cannot play because an error occurred. | ||
| */ | ||
| interface Error : State | ||
| } | ||
|
|
||
| /** | ||
| * State of the playback. | ||
| * | ||
| * @param state The current state. | ||
| * @param playWhenReady If the navigator should play as soon as the state is Ready. | ||
| */ | ||
| data class Playback( | ||
| val state: State, | ||
| val playWhenReady: Boolean | ||
| ) | ||
|
|
||
| /** | ||
| * Indicates the current state of the playback. | ||
| */ | ||
| val playback: StateFlow<Playback> | ||
|
|
||
| val position: StateFlow<P> | ||
|
|
||
| /** | ||
| * Resumes the playback at the current location. | ||
| */ | ||
| fun play() | ||
|
|
||
| /** | ||
| * Pauses the playback. | ||
| */ | ||
| fun pause() | ||
|
|
||
| /** | ||
| * Adapts this navigator to the media3 [Player] interface. | ||
| */ | ||
| fun asPlayer(): Player | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.api | ||
|
|
||
| import kotlinx.coroutines.flow.StateFlow | ||
| import org.readium.r2.shared.ExperimentalReadiumApi | ||
| import org.readium.r2.shared.publication.Locator | ||
|
|
||
| /** | ||
| * A [MediaNavigator] aware of the utterances that are being read aloud. | ||
| */ | ||
| @ExperimentalReadiumApi | ||
| interface SynchronizedMediaNavigator<P : MediaNavigator.Position> : | ||
| MediaNavigator<P> { | ||
|
|
||
| interface Utterance<P : MediaNavigator.Position> { | ||
| val text: String | ||
|
|
||
| val position: P | ||
|
|
||
| val range: IntRange? | ||
|
|
||
| val utteranceLocator: Locator | ||
|
|
||
| val tokenLocator: Locator? | ||
| } | ||
|
|
||
| val utterance: StateFlow<Utterance<P>> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.audio | ||
|
|
||
| import androidx.media3.common.Player | ||
| import kotlin.time.Duration | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import org.readium.r2.navigator.media3.api.MediaNavigator | ||
| import org.readium.r2.navigator.preferences.Configurable | ||
| import org.readium.r2.shared.ExperimentalReadiumApi | ||
|
|
||
| @ExperimentalReadiumApi | ||
| interface AudioEngine<S : Configurable.Settings, P : Configurable.Preferences<P>, E : AudioEngine.Error> : | ||
| Configurable<S, P> { | ||
|
|
||
| interface Error | ||
|
|
||
| data class Playback<E : Error>( | ||
| val state: MediaNavigator.State, | ||
| val playWhenReady: Boolean, | ||
| val error: E? | ||
| ) | ||
|
|
||
| data class Position( | ||
| val index: Int, | ||
| val duration: Duration | ||
| ) | ||
|
|
||
| val playback: StateFlow<Playback<E>> | ||
|
|
||
| val position: StateFlow<Position> | ||
|
|
||
| fun play() | ||
|
|
||
| fun pause() | ||
|
|
||
| fun seek(index: Long, position: Duration) | ||
|
|
||
| fun close() | ||
|
|
||
| fun asPlayer(): Player | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* | ||
| * Copyright 2022 Readium Foundation. All rights reserved. | ||
| * Use of this source code is governed by the BSD-style license | ||
| * available in the top-level LICENSE file of the project. | ||
| */ | ||
|
|
||
| package org.readium.r2.navigator.media3.audio | ||
|
|
||
| import org.readium.r2.navigator.preferences.Configurable | ||
| import org.readium.r2.navigator.preferences.PreferencesEditor | ||
| import org.readium.r2.shared.ExperimentalReadiumApi | ||
| import org.readium.r2.shared.publication.Metadata | ||
| import org.readium.r2.shared.publication.Publication | ||
|
|
||
| @ExperimentalReadiumApi | ||
| interface AudioEngineProvider<S : Configurable.Settings, P : Configurable.Preferences<P>, | ||
| E : PreferencesEditor<P>, F : AudioEngine.Error> { | ||
|
|
||
| suspend fun createEngine(publication: Publication): AudioEngine<S, P, F> | ||
|
|
||
| /** | ||
| * Creates settings for [metadata] and [preferences]. | ||
| */ | ||
| fun computeSettings(metadata: Metadata, preferences: P): S | ||
|
|
||
| /** | ||
| * Creates a preferences editor for [publication] and [initialPreferences]. | ||
| */ | ||
| fun createPreferenceEditor(publication: Publication, initialPreferences: P): E | ||
|
|
||
| /** | ||
| * Creates an empty set of preferences of this TTS engine provider. | ||
| */ | ||
| fun createEmptyPreferences(): P | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.