Skip to content
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

MediaSourceFactory for different content types combined with the dynamic stream links #17

Closed
NikSatyr opened this issue Dec 10, 2021 · 3 comments
Assignees
Labels

Comments

@NikSatyr
Copy link

We're currently improving out media architecture and there are obviously some things that can be done better. Currently, we support HLS & progressive content (DASH is also coming soon), so we need the Player to know what content it's going to play.

Normally, the player is able to figure out the content type itself from the URL when DefaultMediaSourceFactory is used. It takes a look at the item url in createMediaSource(), determines the content type, and based on that loads the correspondent MediaSourceFactory using reflection. So, if the MediaItem url ends with .m3u8, HlsMediaSourceFactory will be used.

However, this approach is not viable for us, since we're fetching the stream links dynamically (more on that here). So the url in MediaItem is not yet a valid one (it contains just information to fetch the link, smth like app://play?media_id=1). The actual url is fetched using ResolvingDataSource.

Previously we were sending an additional field in our media responses from the backend that contained the available transport methods (HLS, DASH etc). Based on that, we were creating a correspondent MediaSourceFactory on the fly (that MediaSourceFactory was created with ResolvingDataSource). However, this seems to be a sort of hack that we'd like to avoid to make our app more scalable (not to mention that this does not fit into media3 architecture at all)

After researching for some time, I've come with an idea. Smth relatively similar was already done here. So I decided to create a custom MediaSource extending CompositeMediaSource. It looks smth like this (unimportant parts are omitted):

class DynamicMediaSource(
    private val mediaItem: MediaItem,
    private val resolvingDataSourceFactory: ResolvingDataSource.Factory,
    private val delegate: DefaultMediaSourceFactory
) : CompositeMediaSource<Int>() {

    private val loader = Loader("-DynamicSource")

    private var actualSource: MediaSource? = null

    override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
        super.prepareSourceInternal(mediaTransferListener)

        val loadable = ParsingLoadable<String>(
            resolvingDataSourceFactory.createDataSource(),
            mediaItem.localConfiguration?.uri ?: return,
            C.DATA_TYPE_MEDIA_INITIALIZATION,
            StreamLinkParser()
        )

        val loaderCallback = object : Loader.Callback<ParsingLoadable<String>> {
            override fun onLoadCompleted(
                loadable: ParsingLoadable<String>,
                elapsedRealtimeMs: Long,
                loadDurationMs: Long
            ) {
                val mediaItemWithStreamLink = mediaItem.buildUpon()
                    .setUri(loadable.result)
                    .build()

                val actualMediaSource = delegate.createMediaSource(mediaItemWithStreamLink)

                actualSource = actualMediaSource

                prepareChildSource(null, actualMediaSource)
            }
        }

        loader.startLoading(
            loadable,
            loaderCallback,
            3
        )
    }

    override fun getMediaItem(): MediaItem {
        return mediaItem
    }

    override fun createPeriod(
        id: MediaSource.MediaPeriodId,
        allocator: Allocator,
        startPositionUs: Long
    ): MediaPeriod {
        return actualSource?.createPeriod(id, allocator, startPositionUs) ?: MaskingMediaPeriod(
            id,
            allocator,
            startPositionUs
        )
    }

    override fun releasePeriod(mediaPeriod: MediaPeriod) {
        actualSource?.releasePeriod(mediaPeriod)
    }

    override fun onChildSourceInfoRefreshed(
        id: Int?,
        mediaSource: MediaSource,
        timeline: Timeline
    ) = refreshSourceInfo(timeline)


    private inner class StreamLinkParser : ParsingLoadable.Parser<String> {
        override fun parse(uri: Uri, inputStream: InputStream): String = uri.toString()
    }

}

As you can see, it extends CompositeMediaSource & with the help of the Loader resolves the actual url. After that, the actual MediaSource is created using the wrapped DefaultMediaSourceFactory & child source is prepared.

This seems to be working, however, there are some things that do not feel right for me:

  1. Passing a MediaSourceFactory to the DynamicMediaSource (I'm not sure if it's ok for MediaSource to call MediaSourceFactory methods)
  2. Overall stability of this approach – I am afraid that at some point I'm going to shoot myself in the foot with this

So is there any better way to achieve what I described or should I go with the code above?

@NikSatyr NikSatyr changed the title MediaSourceFactory for different content types combined with the stream links MediaSourceFactory for different content types combined with the dynamic stream links Dec 10, 2021
@icbaker icbaker self-assigned this Dec 10, 2021
@icbaker
Copy link
Collaborator

icbaker commented Dec 10, 2021

However, this approach is not viable for us, since we're fetching the stream links dynamically (more on that here). So the url in MediaItem is not yet a valid one (it contains just information to fetch the link, smth like app://play?media_id=1). The actual url is fetched using ResolvingDataSource.

Previously we were sending an additional field in our media responses from the backend that contained the available transport methods (HLS, DASH etc). Based on that, we were creating a correspondent MediaSourceFactory on the fly (that MediaSourceFactory was created with ResolvingDataSource). However, this seems to be a sort of hack that we'd like to avoid to make our app more scalable (not to mention that this does not fit into media3 architecture at all)

Possibly obvious question: Given the original 'dynamic' URL do you know whether it will resolve to a DASH or HLS URL? If you do know that in advance then you can just use MediaItem.Builder#setMimeType and everything should just work (this will override any inference done from the URL ending).

Otherwise it sounds like you're looking for a 'do everything' MediaSource. We don't currently have one of those - though there is a draft DefaultMediaSource in a commit linked from google/ExoPlayer#3165 (comment) which you may find useful as inspiration (I haven't compared it in detail to what you've posted).

@NikSatyr
Copy link
Author

Thanks for a quick reply.

Regarding dynamic URL: yes, we could include stream type there & then set up mime type, but that's exactly what we'd like to avoid :)

As for the draft commit you provided – this looks kinda promising. I'll take a deeper dive & get back to you

@NikSatyr
Copy link
Author

I've dug into the example you provided and it's pretty much the same with what I've come up with. Thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants