Skip to content
Open
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 constants.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ project.ext {
desugarJdkLibsVersion = '2.1.5'
lottieVersion = '6.6.0'
truthVersion = '1.4.0'
ktorVersion = '3.0.3'
okhttpVersion = '4.12.0'
testParameterInjectorVersion = '1.18'
modulePrefix = ':'
Expand Down
2 changes: 2 additions & 0 deletions core_settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ include modulePrefix + 'lib-datasource-rtmp'
project(modulePrefix + 'lib-datasource-rtmp').projectDir = new File(rootDir, 'libraries/datasource_rtmp')
include modulePrefix + 'lib-datasource-okhttp'
project(modulePrefix + 'lib-datasource-okhttp').projectDir = new File(rootDir, 'libraries/datasource_okhttp')
include modulePrefix + 'lib-datasource-ktor'
project(modulePrefix + 'lib-datasource-ktor').projectDir = new File(rootDir, 'libraries/datasource_ktor')

include modulePrefix + 'lib-decoder'
project(modulePrefix + 'lib-decoder').projectDir = new File(rootDir, 'libraries/decoder')
Expand Down
47 changes: 47 additions & 0 deletions libraries/datasource_ktor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Ktor DataSource module

This module provides an [HttpDataSource][] implementation that uses [Ktor][].

Ktor is a multiplatform HTTP client developed by JetBrains. It supports HTTP/2,
WebSocket, and Kotlin coroutines for asynchronous operations.

[HttpDataSource]: ../datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java
[Ktor]: https://ktor.io/

## Getting the module

The easiest way to get the module is to add it as a gradle dependency:

```groovy
implementation 'androidx.media3:media3-datasource-ktor:1.X.X'
```

where `1.X.X` is the version, which must match the version of the other media
modules being used.

Alternatively, you can clone this GitHub project and depend on the module
locally. Instructions for doing this can be found in the [top level README][].

[top level README]: ../../README.md

## Using the module

Media components request data through `DataSource` instances. These instances
are obtained from instances of `DataSource.Factory`, which are instantiated and
injected from application code.

If your application only needs to play http(s) content, using the Ktor
extension is as simple as updating any `DataSource.Factory` instantiations in
your application code to use `KtorDataSource.Factory`. If your application
also needs to play non-http(s) content such as local files, use:
```
new DefaultDataSourceFactory(
...
/* baseDataSourceFactory= */ new KtorDataSource.Factory(...));
```

## Links

* [Javadoc][]

[Javadoc]: https://developer.android.com/reference/androidx/media3/datasource/ktor/package-summary
55 changes: 55 additions & 0 deletions libraries/datasource_ktor/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"

apply plugin: 'kotlin-android'

android {
namespace 'androidx.media3.datasource.ktor'

defaultConfig.minSdkVersion project.ext.minSdkVersion

publishing {
singleVariant('release') {
withSourcesJar()
}
}

kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {
api 'io.ktor:ktor-client-core:' + ktorVersion
api 'io.ktor:ktor-client-android:' + ktorVersion
api project(modulePrefix + 'lib-common')
api project(modulePrefix + 'lib-datasource')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + kotlinxCoroutinesVersion
androidTestImplementation project(modulePrefix + 'test-utils')
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion
}

ext {
releaseArtifactId = 'media3-datasource-ktor'
releaseName = 'Media3 Ktor DataSource module'

}
apply from: '../../publish.gradle'
7 changes: 7 additions & 0 deletions libraries/datasource_ktor/proguard-rules.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Proguard rules specific to the Ktor extension.

# Options for Ktor and Okio
-dontwarn io.ktor.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn org.conscrypt.**
28 changes: 28 additions & 0 deletions libraries/datasource_ktor/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.media3.datasource.ktor.test">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk/>

<application
android:usesCleartextTraffic="true" />

<instrumentation
android:targetPackage="androidx.media3.datasource.ktor.test"
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.datasource.ktor

import androidx.media3.datasource.DataSource
import androidx.media3.test.utils.DataSourceContractTest
import androidx.media3.test.utils.HttpDataSourceTestEnv
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.collect.ImmutableList
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeout
import org.junit.Rule
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class KtorDataSourceContractTest : DataSourceContractTest() {

@JvmField @Rule var httpDataSourceTestEnv = HttpDataSourceTestEnv()
val httpClient =
HttpClient() {
install(HttpTimeout) {
requestTimeoutMillis = 400
connectTimeoutMillis = 400
socketTimeoutMillis = 400
}
}

override fun createDataSource(): DataSource {
return KtorDataSource.Factory(httpClient).createDataSource()
}

override fun getTestResources(): ImmutableList<TestResource> {
return httpDataSourceTestEnv.servedResources
}

override fun getNotFoundResources(): MutableList<TestResource> {
return httpDataSourceTestEnv.notFoundResources
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.datasource.ktor

import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.HttpDataSource
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.ktor.client.HttpClient
import java.nio.charset.StandardCharsets
import java.util.HashMap
import java.util.concurrent.TimeUnit
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class KtorDataSourceTest {

val httpClient = HttpClient()

@Test
@Throws(Exception::class)
fun open_setsCorrectHeaders() {
val mockWebServer = MockWebServer()
mockWebServer.enqueue(MockResponse())

val propertyFromFactory = "fromFactory"
val defaultRequestProperties = HashMap<String, String>()
defaultRequestProperties["0"] = propertyFromFactory
defaultRequestProperties["1"] = propertyFromFactory
defaultRequestProperties["2"] = propertyFromFactory
defaultRequestProperties["4"] = propertyFromFactory

val dataSource =
KtorDataSource.Factory(httpClient)
.setDefaultRequestProperties(defaultRequestProperties)
.createDataSource()

val propertyFromSetter = "fromSetter"
dataSource.setRequestProperty("1", propertyFromSetter)
dataSource.setRequestProperty("2", propertyFromSetter)
dataSource.setRequestProperty("3", propertyFromSetter)
dataSource.setRequestProperty("5", propertyFromSetter)

val propertyFromDataSpec = "fromDataSpec"
val dataSpecRequestProperties = HashMap<String, String>()
dataSpecRequestProperties["2"] = propertyFromDataSpec
dataSpecRequestProperties["3"] = propertyFromDataSpec
dataSpecRequestProperties["4"] = propertyFromDataSpec
dataSpecRequestProperties["6"] = propertyFromDataSpec

val dataSpec =
DataSpec.Builder()
.setUri(mockWebServer.url("/test-path").toString())
.setHttpRequestHeaders(dataSpecRequestProperties)
.build()

dataSource.open(dataSpec)

val request = mockWebServer.takeRequest(10, TimeUnit.SECONDS)
assertThat(request).isNotNull()
val headers = request!!.headers
assertThat(headers["0"]).isEqualTo(propertyFromFactory)
assertThat(headers["1"]).isEqualTo(propertyFromSetter)
assertThat(headers["2"]).isEqualTo(propertyFromDataSpec)
assertThat(headers["3"]).isEqualTo(propertyFromDataSpec)
assertThat(headers["4"]).isEqualTo(propertyFromDataSpec)
assertThat(headers["5"]).isEqualTo(propertyFromSetter)
assertThat(headers["6"]).isEqualTo(propertyFromDataSpec)
}

@Test
fun open_invalidResponseCode() {
val mockWebServer = MockWebServer()
mockWebServer.enqueue(MockResponse().setResponseCode(404).setBody("failure msg"))

val dataSource = KtorDataSource.Factory(httpClient).createDataSource()

val dataSpec = DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build()

val exception =
assertThrows(HttpDataSource.InvalidResponseCodeException::class.java) {
dataSource.open(dataSpec)
}

assertThat(exception.responseCode).isEqualTo(404)
assertThat(exception.responseBody).isEqualTo("failure msg".toByteArray(StandardCharsets.UTF_8))
}

@Test
@Throws(Exception::class)
fun factory_setRequestPropertyAfterCreation_setsCorrectHeaders() {
val mockWebServer = MockWebServer()
mockWebServer.enqueue(MockResponse())
val dataSpec = DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build()

val factory = KtorDataSource.Factory(httpClient)
val dataSource = factory.createDataSource()

val defaultRequestProperties = HashMap<String, String>()
defaultRequestProperties["0"] = "afterCreation"
factory.setDefaultRequestProperties(defaultRequestProperties)
dataSource.open(dataSpec)

val request = mockWebServer.takeRequest(10, TimeUnit.SECONDS)
assertThat(request).isNotNull()
val headers = request!!.headers
assertThat(headers["0"]).isEqualTo("afterCreation")
}

@Test
fun open_malformedUrl_throwsException() {
val dataSource = KtorDataSource.Factory(httpClient).createDataSource()

val dataSpec = DataSpec.Builder().setUri("not-a-valid-url").build()

val exception =
assertThrows(HttpDataSource.HttpDataSourceException::class.java) { dataSource.open(dataSpec) }

assertThat(exception.message).contains("Malformed URL")
}

@Test
@Throws(Exception::class)
fun open_httpPost_sendsPostRequest() {
val mockWebServer = MockWebServer()
mockWebServer.enqueue(MockResponse())

val dataSource = KtorDataSource.Factory(httpClient).createDataSource()

val dataSpec =
DataSpec.Builder()
.setUri(mockWebServer.url("/test-path").toString())
.setHttpMethod(DataSpec.HTTP_METHOD_POST)
.setHttpBody("test body".toByteArray(StandardCharsets.UTF_8))
.build()

dataSource.open(dataSpec)

val request = mockWebServer.takeRequest(10, TimeUnit.SECONDS)
assertThat(request).isNotNull()
assertThat(request!!.method).isEqualTo("POST")
assertThat(request.body.readUtf8()).isEqualTo("test body")
}
}
18 changes: 18 additions & 0 deletions libraries/datasource_ktor/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="androidx.media3.datasource.ktor">
<uses-sdk />
</manifest>
Loading