-
Notifications
You must be signed in to change notification settings - Fork 324
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
Support for playing Kotlin Multiplatform resources #1405
Comments
I can see two possible options here:
(2) seems to solve the problem described here, without the fiddliness introduced by (1). The downside of (2) seems to be that we can only read the entire file into memory in one go, which might not be great for large media files. On the other hand, large media files probably shouldn't be baked into the APK in the first place... I'm tempted to go with (2), and maybe in future Compose Multi-Platform will add some first-class support for reading resources 'incrementally' and we can update our implementation to use that. |
Ah, I didn't realise that the
This makes (2) a little trickier, though we can either take a super-type of this generated |
EDIT: The suggestion in this comment of creating a separate
public final class ResolvingByteArrayDataSource implements DataSource {
public static final class Factory implements DataSource.Factory {
private final Function<Uri, byte[]> uriResolver;
public Factory(Function<Uri, byte[]> uriResolver) {
this.uriResolver = uriResolver;
}
@Override
public DataSource createDataSource() {
return new ResolvingByteArrayDataSource(uriResolver);
}
}
private final Function<Uri, byte[]> uriResolver;
private final ArrayList<TransferListener> listeners;
@Nullable private ByteArrayDataSource delegate;
@Nullable private Uri uri;
private ResolvingByteArrayDataSource(Function<Uri, byte[]> uriResolver) {
this.uriResolver = uriResolver;
this.listeners = new ArrayList<>(/* initialCapacity= */ 1);
}
@Override
public long open(DataSpec dataSpec) throws IOException {
uri = dataSpec.uri;
delegate = new ByteArrayDataSource(uriResolver.apply(uri));
for (TransferListener listener : listeners) {
delegate.addTransferListener(listener);
}
return delegate.open(dataSpec);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
return delegate.read(buffer, offset, length);
}
@Override
public void addTransferListener(TransferListener transferListener) {
if (!listeners.contains(transferListener)) {
listeners.add(transferListener);
}
if (delegate != null) {
delegate.addTransferListener(transferListener);
}
}
@Nullable
@Override
public Uri getUri() {
return uri;
}
@Override
public void close() throws IOException {
uri = null;
delegate.close();
delegate = null;
}
} Then you would set your player up with this (see https://developer.android.com/media/media3/exoplayer/customization) - this makes your player only work with 'kotlin multi platform' URIs: ExoPlayer player =
new ExoPlayer.Builder(/* context= */ this)
.setMediaSourceFactory(
new DefaultMediaSourceFactory(
new ByteArrayDataSource.Factory(uri -> Res.readBytes(uri.getPath())))
.build(); And you should then be able to play a resource like this: player.addMediaItem(MediaItem.fromUri("files/music.mp3")); |
First thing - great thank to you for all the help and ideas so far! I really appreciate it. I have tested code above and it's working. I have few things if someone will reuse it too - possibly from Kotlin code: readBytes() is suspend function so it needs to be called similar to this
b) Don't forget to add also As you mentioned above about delaying instantiation of In case that option (2) is way to go - should I create new issue on Compose Multiplatform side about Res.readBytes() reading in on go as you mentioned above? Or it will be more clear what kind of interface will be needed as this will be implemented on this side? In short - let me know if I can do something more. And thanks again. |
@Woren I am assuming you don't have a lot of files at hand, a little workaround approach to do is via offloading the files inside the jar to the app's contextual private files directory (Which on Android equals to |
@yuroyami Before Compose Multiplatform Resources generation (since 1.6.0) I was using mentioned But as mentioned before workaround using uriResolver seams to be working for now so hopefully until proper solution by Media3/Exoplayer library there will be no need another one. But thanks for the idea! I will keep in mind for another possible problems in other areas. And surely there will be some. |
This is a relatively small change, and massively simplifies the work needed for an app to consume Kotlin Multiplatform resources (without a full `KmpResourceDataSource` implementation, which poses some dependency challenges for now). Issue: #1405 PiperOrigin-RevId: 638991375
4dd8360 changes the library's We are not currently planning any tighter integration with Kotlin Multiplatform Resources - partly because it looks like it will require some additional dependencies that are currently tricky to resolve in the environments where we develop and deploy this library (and it would probably also need to be in a new extension module). I will leave this issue open to track doing something more closely integrated in future. |
Thank you for changes with simplifications and more info about the future. I will check mentioned commit when the new version is out. Version 1.6.0 of Compose/Kotlin Multiplatform is one of the first versions with this support (now for example lacking handling larger files) so it will be more mature in coming months/years.
|
If someone is still looking at this, and want a more optimized solution that doesn't load the whole content of the file to the memory, here is an attempt at creating a custom class JarResourceDataSource : BaseDataSource(false) {
private var uri: Uri? = null
private var inputStream: InputStream? = null
private var bytesRemaining = 0
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (length == 0) return 0
if (bytesRemaining == 0) return C.RESULT_END_OF_INPUT
val read = inputStream!!.read(buffer, offset, min(bytesRemaining, length))
if (read == -1) {
return C.RESULT_END_OF_INPUT
} else {
bytesRemaining -= read
bytesTransferred(read)
}
return read
}
override fun open(dataSpec: DataSpec): Long {
uri = dataSpec.uri
transferInitializing(dataSpec)
inputStream = URL(dataSpec.uri.toString()).openConnection().getInputStream()
bytesRemaining = if (dataSpec.length == C.LENGTH_UNSET.toLong()) {
inputStream!!.available() - dataSpec.position.toInt()
} else {
dataSpec.length.toInt()
}
return bytesRemaining.toLong()
}
override fun getUri(): Uri? = uri
override fun close() {
inputStream?.close()
uri = null
inputStream = null
}
} I think this is similar to the approach (1) that @icbaker initially suggested, just instead of having to think about where the Jar is located and other stuff, we just delegate the call to Now, we can consume this directly same as the suggestions above by passing a custom class ExtendedMediaFactory(private val defaultMediaFactory: DefaultMediaSourceFactory) :
MediaSource.Factory by defaultMediaFactory {
private val jarResourceMediaSourceFactory = ProgressiveMediaSource.Factory {
JarResourceDataSource()
}
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
return if (mediaItem.localConfiguration?.uri?.scheme == "jar") {
jarResourceMediaSourceFactory.createMediaSource(mediaItem)
} else {
defaultMediaFactory.createMediaSource(mediaItem)
}
}
}
----
ExoPlayer.Builder(context).setMediaSourceFactory(ExtendedMediaFactory(DefaultMediaSourceFactory(context))) Then, you should be able to play the media: player.addMediaItem(MediaItem.fromUri(Res.getUri("files/media.mp3"))); |
Thanks for the code suggestions @hichamboushaba - a couple of thoughts if you or others are interested:
|
Thank you @icbaker, this is quite helpful, I didn't know about these two points. |
No problem - some other thoughts: One of the problems with building a
Instead of this, I wonder if you can use You current implementation also ignores |
Thanks again @icbaker
I used
That's correct, I missed this, thanks for pointing it. |
Use case description
Main use case is about playing files inside APK with not only for example Uri:
Uri.parse(("file:///android_asset/music.mp3"))
But to be able to use files located in jar archive. For example Uri:
jar:file:/data/app/~~4hhLse7uFXE7V7sA==/com.example.composetest-jD6eQ3BeyvfQ==/base.apk!/composeResources/com.example.shared.resources/files/music.mp3
This approache can be used for multiplatform applications (e.i. multiplatform media players using AVPlayer on iOS and Exoplayer on Android with shared code) using Compose Multiplatform which will generated resources Uri for files inside common/shared directory. Mentioned directory "android_asset" is not accessible on iOS and generated resources Uri can't be used on Android. Media files needs to be duplicated to proper platform dependend directories.
More about Compose Multiplatform resources:
https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-images-resources.html
Proposed solution
Be able to use following code:
val mediaItem = MediaItem.Builder().setUri(Res.getUri("files/music.mp3")).build()
Alternatives considered
Only solution I can think of is using custom MediaSource and handling InputStream using method from Compose Multiplatform:
Res.readBytes("files/music.mp3")
with solution mentioned for example here (Exoplayer issue)
Similar issue is opened in Compose Multiplatform repository here and support from this side is not coming. The reasons are mentioned there.
Thank you for consideration!
The text was updated successfully, but these errors were encountered: