/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer import android.net.Uri import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec import androidx.media3.datasource.TransferListener import androidx.media3.extractor.metadata.icy.IcyHeaders import com.doublesymmetry.trackplayer.models.MediaHeaders import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config /** * Verifies [HeaderInjectingDataSourceFactory] forwards every [DataSource] * member (including Java `default` ones like `getResponseHeaders`) to the * underlying source, and that per-item headers override defaults from the * incoming [DataSpec]. Regression coverage for #2638. */ @OptIn(UnstableApi::class) @RunWith(RobolectricTestRunner::class) @Config(sdk = [33], manifest = Config.NONE) class HeaderInjectingDataSourceFactoryTest { @After fun tearDown() { MediaHeaders.clear() } @Test fun `wrapper forwards getResponseHeaders to underlying source`() { val stub = StubDataSource() val ds = wrap(stub) ds.open(DataSpec.Builder().setUri(Uri.parse("https://example.com/stream")).build()) val headers = ds.responseHeaders val parsed = IcyHeaders.parse(headers) assertNotNull("Wrapper returned empty response headers (regression of #2638)", parsed) assertEquals(16_000, parsed!!.metadataInterval) assertEquals(192_000, parsed.bitrate) assertEquals("Test Radio", parsed.name) } @Test fun `wrapper forwards getUri to underlying source`() { val stub = StubDataSource() val ds = wrap(stub) val uri = Uri.parse("https://example.com/stream") ds.open(DataSpec.Builder().setUri(uri).build()) assertEquals(uri, ds.uri) } @Test fun `wrapper forwards read to underlying source`() { val stub = StubDataSource() val ds = wrap(stub) ds.open(DataSpec.Builder().setUri(Uri.parse("https://example.com/stream")).build()) val buf = ByteArray(StubDataSource.PAYLOAD.size) val n = ds.read(buf, 0, buf.size) assertEquals(StubDataSource.PAYLOAD.size, n) assertEquals(StubDataSource.PAYLOAD.toList(), buf.toList()) } @Test fun `wrapper forwards addTransferListener to underlying source`() { val stub = StubDataSource() val ds = wrap(stub) val listener = object : TransferListener { override fun onTransferInitializing(s: DataSource, d: DataSpec, n: Boolean) {} override fun onTransferStart(s: DataSource, d: DataSpec, n: Boolean) {} override fun onBytesTransferred(s: DataSource, d: DataSpec, n: Boolean, b: Int) {} override fun onTransferEnd(s: DataSource, d: DataSpec, n: Boolean) {} } ds.addTransferListener(listener) assertEquals(listOf(listener), stub.transferListeners) } @Test fun `per-item headers override Media3 defaults`() { val uriStr = "https://example.com/stream" MediaHeaders.set(uriStr, mapOf("Icy-MetaData" to "0", "X-Custom" to "hello")) val stub = StubDataSource() val ds = wrap(stub) ds.open( DataSpec.Builder() .setUri(Uri.parse(uriStr)) .setHttpRequestHeaders(mapOf("Icy-MetaData" to "1", "Other" to "default")) .build(), ) val seen = stub.lastSpec!!.httpRequestHeaders assertEquals("0", seen["Icy-MetaData"]) assertEquals("default", seen["Other"]) assertEquals("hello", seen["X-Custom"]) } @Test fun `dataSpec passes through unchanged when no per-item headers`() { val stub = StubDataSource() val ds = wrap(stub) val original = DataSpec.Builder() .setUri(Uri.parse("https://example.com/stream")) .setHttpRequestHeaders(mapOf("Icy-MetaData" to "1")) .build() ds.open(original) assertEquals(mapOf("Icy-MetaData" to "1"), stub.lastSpec!!.httpRequestHeaders) } // ---- helpers ---- private fun wrap(stub: DataSource): DataSource { return HeaderInjectingDataSourceFactory(DataSource.Factory { stub }).createDataSource() } /** Returns canned ICY response headers in the Shoutcast/Icecast wire format. */ private class StubDataSource : DataSource { var lastSpec: DataSpec? = null private set val transferListeners = mutableListOf() private var openedUri: Uri? = null private var readCursor = 0 override fun open(dataSpec: DataSpec): Long { lastSpec = dataSpec openedUri = dataSpec.uri readCursor = 0 return PAYLOAD.size.toLong() } override fun addTransferListener(transferListener: TransferListener) { transferListeners.add(transferListener) } override fun getUri(): Uri? = openedUri override fun getResponseHeaders(): Map> = mapOf( "Content-Type" to listOf("audio/mpeg"), "icy-br" to listOf("192"), "icy-name" to listOf("Test Radio"), "icy-pub" to listOf("1"), "icy-metaint" to listOf("16000"), ) override fun read(buffer: ByteArray, offset: Int, length: Int): Int { if (readCursor >= PAYLOAD.size) return -1 val n = minOf(length, PAYLOAD.size - readCursor) System.arraycopy(PAYLOAD, readCursor, buffer, offset, n) readCursor += n return n } override fun close() {} companion object { val PAYLOAD = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05) } } }