From 1e4f89954825b74d4064b1bb13e35f6a3b9a6759 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 18 Aug 2017 06:30:21 -0700 Subject: [PATCH] Add support for the data URI scheme ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165699328 --- .../upstream/DataSchemeDataSourceTest.java | 90 +++++++++++++++++++ .../java/com/google/android/exoplayer2/C.java | 4 + .../upstream/DataSchemeDataSource.java | 88 ++++++++++++++++++ .../upstream/DefaultDataSource.java | 13 ++- 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java new file mode 100644 index 0000000000..5ba9e18e7d --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 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. + */ +package com.google.android.exoplayer2.upstream; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DataSchemeDataSource}. + */ +public final class DataSchemeDataSourceTest extends TestCase { + + private DataSource schemeDataDataSource; + + @Override + public void setUp() { + schemeDataDataSource = new DataSchemeDataSource(); + } + + public void testBase64Data() throws IOException { + DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" + + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" + + "DAwMDAwMDAwMDAiXX0="); + TestUtil.assertDataSourceContent(schemeDataDataSource, dataSpec, + ("{\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}").getBytes()); + } + + public void testAsciiData() throws IOException { + TestUtil.assertDataSourceContent(schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"), + "A brief note".getBytes()); + } + + public void testPartialReads() throws IOException { + byte[] buffer = new byte[18]; + DataSpec dataSpec = buildDataSpec("data:,012345678901234567"); + assertEquals(18, schemeDataDataSource.open(dataSpec)); + assertEquals(9, schemeDataDataSource.read(buffer, 0, 9)); + assertEquals(0, schemeDataDataSource.read(buffer, 3, 0)); + assertEquals(9, schemeDataDataSource.read(buffer, 9, 15)); + assertEquals(0, schemeDataDataSource.read(buffer, 1, 0)); + assertEquals(C.RESULT_END_OF_INPUT, schemeDataDataSource.read(buffer, 1, 1)); + assertEquals("012345678901234567", new String(buffer, 0, 18)); + } + + public void testIncorrectScheme() { + try { + schemeDataDataSource.open(buildDataSpec("http://www.google.com")); + fail(); + } catch (IOException e) { + // Expected. + } + } + + public void testMalformedData() { + try { + schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content")); + fail(); + } catch (IOException e) { + // Expected. + } + try { + schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,IncorrectPadding==")); + fail(); + } catch (IOException e) { + // Expected. + } + } + + private static DataSpec buildDataSpec(String uriString) { + return new DataSpec(Uri.parse(uriString)); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 1cfcded1cb..e25538a062 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -73,6 +73,10 @@ public final class C { */ public static final long NANOS_PER_SECOND = 1000000000L; + /** + * The name of the ASCII charset. + */ + public static final String ASCII_NAME = "US-ASCII"; /** * The name of the UTF-8 charset. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java new file mode 100644 index 0000000000..c547625819 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 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. + */ +package com.google.android.exoplayer2.upstream; + +import android.net.Uri; +import android.util.Base64; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import java.io.IOException; +import java.net.URLDecoder; + +/** + * A {@link DataSource} for reading data URLs, as defined by RFC 2397. + */ +public final class DataSchemeDataSource implements DataSource { + + public static final String SCHEME_DATA = "data"; + + private DataSpec dataSpec; + private int bytesRead; + private byte[] data; + + @Override + public long open(DataSpec dataSpec) throws IOException { + this.dataSpec = dataSpec; + Uri uri = dataSpec.uri; + String scheme = uri.getScheme(); + if (!SCHEME_DATA.equals(scheme)) { + throw new ParserException("Unsupported scheme: " + scheme); + } + String[] uriParts = uri.getSchemeSpecificPart().split(","); + if (uriParts.length > 2) { + throw new ParserException("Unexpected URI format: " + uri); + } + String dataString = uriParts[1]; + if (uriParts[0].contains(";base64")) { + try { + data = Base64.decode(dataString, 0); + } catch (IllegalArgumentException e) { + throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); + } + } else { + // TODO: Add support for other charsets. + data = URLDecoder.decode(dataString, C.ASCII_NAME).getBytes(); + } + return data.length; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) { + if (readLength == 0) { + return 0; + } + int remainingBytes = data.length - bytesRead; + if (remainingBytes == 0) { + return C.RESULT_END_OF_INPUT; + } + readLength = Math.min(readLength, remainingBytes); + System.arraycopy(data, bytesRead, buffer, offset, readLength); + bytesRead += readLength; + return readLength; + } + + @Override + public Uri getUri() { + return dataSpec != null ? dataSpec.uri : null; + } + + @Override + public void close() throws IOException { + dataSpec = null; + data = null; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index cbb8ba92a5..853b40f73f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -34,10 +34,11 @@ import java.lang.reflect.InvocationTargetException; *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). *
  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension.
  • + *
  • data: For parsing data inlined in the URI as defined in RFC 2397.
  • *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if * constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or * any other schemes supported by a base data source if constructed using - * {@link #DefaultDataSource(Context, TransferListener, DataSource)}. + * {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
  • * */ public final class DefaultDataSource implements DataSource { @@ -58,6 +59,7 @@ public final class DefaultDataSource implements DataSource { private DataSource assetDataSource; private DataSource contentDataSource; private DataSource rtmpDataSource; + private DataSource dataSchemeDataSource; private DataSource dataSource; @@ -130,6 +132,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); + } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { + dataSource = getDataSchemeDataSource(); } else { dataSource = baseDataSource; } @@ -202,4 +206,11 @@ public final class DefaultDataSource implements DataSource { return rtmpDataSource; } + private DataSource getDataSchemeDataSource() { + if (dataSchemeDataSource == null) { + dataSchemeDataSource = new DataSchemeDataSource(); + } + return dataSchemeDataSource; + } + }