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

Add MPEG-DASH support to playback #27

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Armadillo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {

implementation "com.google.android.exoplayer:exoplayer-core:${EXOPLAYER_VERSION}"
implementation "com.google.android.exoplayer:exoplayer-hls:${EXOPLAYER_VERSION}"
implementation "com.google.android.exoplayer:exoplayer-dash:${EXOPLAYER_VERSION}"
implementation "com.google.android.exoplayer:extension-mediasession:${EXOPLAYER_VERSION}"

implementation "io.reactivex.rxjava2:rxjava:${RXJAVA_VERSION}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.scribd.armadillo.playback.MediaMetadataCompatBuilderImpl
import com.scribd.armadillo.playback.PlaybackEngineFactoryHolder
import com.scribd.armadillo.playback.PlaybackStateBuilderImpl
import com.scribd.armadillo.playback.PlaybackStateCompatBuilder
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelper
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceHelperImpl
import com.scribd.armadillo.playback.mediasource.MediaSourceRetriever
import com.scribd.armadillo.playback.mediasource.MediaSourceRetrieverImpl
import dagger.Module
Expand Down Expand Up @@ -56,4 +58,8 @@ internal class PlaybackModule {
@Provides
@Singleton
fun mediaSourceRetriever(mediaSourceRetrieverImpl: MediaSourceRetrieverImpl): MediaSourceRetriever = mediaSourceRetrieverImpl

@Provides
@Singleton
fun mediaSourceHelper(mediaSourceHelperImpl: HeadersMediaSourceHelperImpl): HeadersMediaSourceHelper = mediaSourceHelperImpl
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.offline.DownloadRequest
import com.google.android.exoplayer2.offline.DownloadService
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.Util
import com.scribd.armadillo.Constants
Expand Down Expand Up @@ -94,7 +93,8 @@ internal class ExoplayerDownloadEngine @Inject constructor(private val context:
.setUri(uri)
.build()
return when (@C.ContentType val type = Util.inferContentType(uri)) {
C.TYPE_HLS ->
C.TYPE_HLS,
C.TYPE_DASH ->
DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, DefaultDataSource.Factory(context, dataSourceFactory))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is DASH likely to diverge from HLS? If not, you should combine these cases.

C.TYPE_OTHER -> DownloadHelper.forMediaItem(context, mediaItem)
else -> throw IllegalStateException("Unsupported type: $type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink
import com.google.android.exoplayer2.audio.DefaultAudioSink.DefaultAudioProcessorChain
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.dash.manifest.DashManifest
import com.google.android.exoplayer2.source.hls.HlsManifest
import com.scribd.armadillo.Milliseconds
import com.scribd.armadillo.time.milliseconds
Expand All @@ -20,13 +21,12 @@ import com.scribd.armadillo.time.milliseconds
* During setup, [ExoPlayer.getCurrentManifest] will be null.
*/
internal fun ExoPlayer.hasProgressAvailable(): Boolean {
return (isPlayingHls() && (currentManifest as HlsManifest).mediaPlaylist.durationUs != C.TIME_UNSET)
||
(currentManifest == null && !currentTimeline.isEmpty)
return when (val m = currentManifest) {
is HlsManifest -> m.mediaPlaylist.durationUs != C.TIME_UNSET
is DashManifest -> m.durationMs != C.TIME_UNSET
else -> m == null && !currentTimeline.isEmpty
}
}

internal fun ExoPlayer.isPlayingHls(): Boolean = currentManifest is HlsManifest

/**
* Current position in relation to all audio files.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.scribd.armadillo.playback.mediasource

import android.content.Context
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadHelper
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.scribd.armadillo.download.DownloadTracker
import com.scribd.armadillo.extensions.toUri
import com.scribd.armadillo.models.AudioPlayable
import javax.inject.Inject

internal class DashMediaSourceGenerator @Inject constructor(
private val mediaSourceHelper: HeadersMediaSourceHelper,
private val downloadTracker: DownloadTracker) : MediaSourceGenerator {

override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource {
val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request)

downloadTracker.getDownload(request.url.toUri())?.let {
if (it.state != Download.STATE_FAILED) {
return DownloadHelper.createMediaSource(it.request, dataSourceFactory)
}
}

val mediaItem = MediaItem.Builder()
.setUri(request.url)
.build()

return DashMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
}

override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) = mediaSourceHelper.updateMediaSourceHeaders(request)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.scribd.armadillo.playback.mediasource

import android.content.Context
import com.google.android.exoplayer2.upstream.DataSource
import com.scribd.armadillo.models.AudioPlayable

internal interface HeadersMediaSourceHelper {
fun createDataSourceFactory(context: Context, request: AudioPlayable.MediaRequest): DataSource.Factory
fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.scribd.armadillo.playback.mediasource

import android.content.Context
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.scribd.armadillo.Constants
import com.scribd.armadillo.HeadersStore
import com.scribd.armadillo.download.CacheManager
import com.scribd.armadillo.models.AudioPlayable
import javax.inject.Inject
import javax.inject.Singleton

@Singleton

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what @Singleton does here? I thought it only needs to be on the provides method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right, but I think it's helpful to see it here to see that it's a singleton, otherwise it's hidden.

internal class HeadersMediaSourceHelperImpl @Inject constructor(
private val cacheManager: CacheManager,
private val headersStore: HeadersStore
): HeadersMediaSourceHelper {
private val previousRequests = mutableMapOf<String, DefaultHttpDataSource.Factory>()

override fun createDataSourceFactory(context: Context, request: AudioPlayable.MediaRequest): DataSource.Factory {
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
.setUserAgent(Constants.getUserAgent(context))
.setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS)
.setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS)
.setAllowCrossProtocolRedirects(true)

previousRequests[request.url] = httpDataSourceFactory
if (request.headers.isNotEmpty()) {
headersStore.keyForUrl(request.url)?.let {
headersStore.setHeaders(it, request.headers)
}
httpDataSourceFactory.setDefaultRequestProperties(request.headers)
}

val upstreamFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
return cacheManager.playbackDataSourceFactory(context, upstreamFactory)

}

override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) {
previousRequests[request.url]?.let { factory ->
if (request.headers.isNotEmpty()) {
headersStore.keyForUrl(request.url)?.let {
headersStore.setHeaders(it, request.headers)
}
// Updating the factory instance updates future requests generated from this factory by ExoPlayer
factory.setDefaultRequestProperties(request.headers)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadHelper
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.scribd.armadillo.Constants
import com.scribd.armadillo.HeadersStore
import com.scribd.armadillo.download.CacheManager
import com.scribd.armadillo.download.DownloadTracker
import com.scribd.armadillo.extensions.toUri
import com.scribd.armadillo.models.AudioPlayable
Expand All @@ -22,50 +16,21 @@ import javax.inject.Inject
*
*/
internal class HlsMediaSourceGenerator @Inject constructor(
private val cacheManager: CacheManager,
private val headersStore: HeadersStore,
private val mediaSourceHelper: HeadersMediaSourceHelper,
private val downloadTracker: DownloadTracker) : MediaSourceGenerator {

private val previousRequests = mutableMapOf<String, DefaultHttpDataSource.Factory>()

override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource {
val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request)

downloadTracker.getDownload(request.url.toUri())?.let {
if (it.state != Download.STATE_FAILED) {
return DownloadHelper.createMediaSource(it.request, buildDataSourceFactory(context, request))
return DownloadHelper.createMediaSource(it.request, dataSourceFactory)
}
}
return HlsMediaSource.Factory(buildDataSourceFactory(context, request))
return HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(request.url))
}

override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) {
previousRequests[request.url]?.let { factory ->
if (request.headers.isNotEmpty()) {
headersStore.keyForUrl(request.url)?.let {
headersStore.setHeaders(it, request.headers)
}
// Updating the factory instance updates future requests generated from this factory by ExoPlayer
factory.setDefaultRequestProperties(request.headers)
}
}
}

private fun buildDataSourceFactory(context: Context, request: AudioPlayable.MediaRequest): DataSource.Factory {
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
.setUserAgent(Constants.getUserAgent(context))
.setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS)
.setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS)
.setAllowCrossProtocolRedirects(true)

previousRequests[request.url] = httpDataSourceFactory
if (request.headers.isNotEmpty()) {
headersStore.keyForUrl(request.url)?.let {
headersStore.setHeaders(it, request.headers)
}
httpDataSourceFactory.setDefaultRequestProperties(request.headers)
}

val upstreamFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
return cacheManager.playbackDataSourceFactory(context, upstreamFactory)
}
override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) = mediaSourceHelper.updateMediaSourceHeaders(request)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class MediaSourceRetrieverImpl @Inject constructor(): MediaSourceRetriever {
@Inject
internal lateinit var hlsGenerator: HlsMediaSourceGenerator

@Inject
internal lateinit var dashGenerator: DashMediaSourceGenerator

@Inject
internal lateinit var progressiveMediaSourceGenerator: ProgressiveMediaSourceGenerator

Expand All @@ -46,6 +49,7 @@ class MediaSourceRetrieverImpl @Inject constructor(): MediaSourceRetriever {

return when (@C.ContentType val type = Util.inferContentType(uri, overrideExtension)) {
C.TYPE_HLS -> hlsGenerator
C.TYPE_DASH -> dashGenerator
C.TYPE_OTHER -> progressiveMediaSourceGenerator
else -> throw IllegalStateException("Unsupported type: $type")
}
Expand Down
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Project Armadillo Release Notes

## 1.2.0
- Adds support for MPEG-DASH audio

## 1.1.1
- Added a fix for seek from notification player

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
PACKAGE_NAME=com.scribd.armadillo
LIBRARY_VERSION=1.1.1
LIBRARY_VERSION=1.2.0
EXOPLAYER_VERSION=2.17.1
RXJAVA_VERSION=2.2.4
RXANDROID_VERSION=2.0.1
Expand Down
Loading