Add JSON asset list parser

PiperOrigin-RevId: 697966021
This commit is contained in:
bachinger 2024-11-19 04:37:32 -08:00 committed by Copybara-Service
parent 98723dc0a8
commit b9c9d95b90
3 changed files with 439 additions and 0 deletions

View File

@ -0,0 +1,208 @@
/*
* Copyright 2024 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 androidx.media3.exoplayer.hls;
import android.net.Uri;
import android.util.JsonReader;
import android.util.JsonToken;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Objects;
/** Parses a X-ASSET-LIST JSON object. */
/* package */ final class AssetListParser
implements ParsingLoadable.Parser<AssetListParser.AssetList> {
/** Holds assets. */
public static final class AssetList {
private static final AssetList EMPTY = new AssetList(ImmutableList.of(), ImmutableList.of());
/** The list of assets. */
public final ImmutableList<Asset> assets;
/** The list of string attributes of the asset list JSON object. */
public final ImmutableList<StringAttribute> stringAttributes;
/** Creates an instance. */
public AssetList(ImmutableList<Asset> assets, ImmutableList<StringAttribute> stringAttributes) {
this.assets = assets;
this.stringAttributes = stringAttributes;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AssetList)) {
return false;
}
AssetList assetList = (AssetList) o;
return Objects.equals(assets, assetList.assets)
&& Objects.equals(stringAttributes, assetList.stringAttributes);
}
@Override
public int hashCode() {
return Objects.hash(assets, stringAttributes);
}
}
/**
* An asset with a URI and a duration.
*
* <p>See RFC 8216bis, appendix D.2, X-ASSET-LIST.
*/
public static final class Asset {
/** A uri to an HLS source. */
public final Uri uri;
/** The duration, in microseconds. */
public final long durationUs;
/** Creates an instance. */
public Asset(Uri uri, long durationUs) {
this.uri = uri;
this.durationUs = durationUs;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Asset)) {
return false;
}
Asset asset = (Asset) o;
return durationUs == asset.durationUs && Objects.equals(uri, asset.uri);
}
@Override
public int hashCode() {
return Objects.hash(uri, durationUs);
}
}
/** A string attribute with its name and value. */
public static final class StringAttribute {
/** The name of the attribute. */
public final String name;
/** The value of the attribute. */
public final String value;
/** Creates an instance. */
public StringAttribute(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof StringAttribute)) {
return false;
}
StringAttribute that = (StringAttribute) o;
return Objects.equals(name, that.name) && Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(name, value);
}
}
/** The asset name of the assets array in a X-ASSET-LIST JSON object. */
private static final String ASSET_LIST_JSON_NAME_ASSET_ARRAY = "ASSETS";
/** The asset URI name in a X-ASSET-LIST JSON object. */
private static final String ASSET_LIST_JSON_NAME_URI = "URI";
/** The asset duration name in a X-ASSET-LIST JSON object. */
private static final String ASSET_LIST_JSON_NAME_DURATION = "DURATION";
@Override
public AssetList parse(Uri uri, InputStream inputStream) throws IOException {
try (JsonReader reader = new JsonReader(new InputStreamReader(inputStream))) {
if (reader.peek() != JsonToken.BEGIN_OBJECT) {
return AssetList.EMPTY;
}
ImmutableList.Builder<Asset> assets = new ImmutableList.Builder<>();
ImmutableList.Builder<StringAttribute> stringAttributes = new ImmutableList.Builder<>();
reader.beginObject();
while (reader.hasNext()) {
JsonToken token = reader.peek();
if (token.equals(JsonToken.NAME)) {
String name = reader.nextName();
if (name.equals(ASSET_LIST_JSON_NAME_ASSET_ARRAY)
&& reader.peek() == JsonToken.BEGIN_ARRAY) {
parseAssetArray(reader, assets);
} else if (reader.peek() == JsonToken.STRING) {
stringAttributes.add(new StringAttribute(name, reader.nextString()));
} else {
reader.skipValue();
}
}
}
return new AssetList(assets.build(), stringAttributes.build());
}
}
private static void parseAssetArray(JsonReader reader, ImmutableList.Builder<Asset> assets)
throws IOException {
reader.beginArray();
while (reader.hasNext()) {
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
parseAssetObject(reader, assets);
}
}
reader.endArray();
}
private static void parseAssetObject(JsonReader reader, ImmutableList.Builder<Asset> assets)
throws IOException {
reader.beginObject();
String uri = null;
long duration = C.TIME_UNSET;
String name;
while (reader.hasNext()) {
name = reader.nextName();
if (name.equals(ASSET_LIST_JSON_NAME_URI) && reader.peek() == JsonToken.STRING) {
uri = reader.nextString();
} else if (name.equals(ASSET_LIST_JSON_NAME_DURATION) && reader.peek() == JsonToken.NUMBER) {
duration = (long) (reader.nextDouble() * C.MICROS_PER_SECOND);
} else {
reader.skipValue();
}
}
if (uri != null && duration != C.TIME_UNSET) {
assets.add(new Asset(Uri.parse(uri), duration));
}
reader.endObject();
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright 2024 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 androidx.media3.exoplayer.hls;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.datasource.ByteArrayDataSource;
import androidx.media3.exoplayer.hls.AssetListParser.Asset;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class AssetListParserTest {
@Test
public void load() throws IOException {
byte[] assetListBytes =
("{\"ASSETS\": [ "
+ "{\"URI\": \"http://1\", \"DURATION\":1.23},"
+ "{\"URI\": \"http://2\", \"DURATION\":2.34}"
+ "] }")
.getBytes(Charset.defaultCharset());
ParsingLoadable<AssetListParser.AssetList> parsingLoadable =
new ParsingLoadable<>(
new ByteArrayDataSource(assetListBytes),
Uri.EMPTY,
C.DATA_TYPE_AD,
new AssetListParser());
parsingLoadable.load();
assertThat(parsingLoadable.getResult().assets)
.containsExactly(
new Asset(Uri.parse("http://1"), /* durationUs= */ 1_230_000L),
new Asset(Uri.parse("http://2"), /* durationUs= */ 2_340_000L))
.inOrder();
}
@Test
public void load_fileWithDisturbingJsonJunk_parsesCorrectly() throws IOException {
byte[] assetListBytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(),
"media/hls/interstitials/x_asset_list_mixed_elements.json");
ParsingLoadable<AssetListParser.AssetList> parsingLoadable =
new ParsingLoadable<>(
new ByteArrayDataSource(assetListBytes),
Uri.EMPTY,
C.DATA_TYPE_AD,
new AssetListParser());
parsingLoadable.load();
assertThat(parsingLoadable.getResult().assets)
.containsExactly(
new Asset(Uri.parse("http://1"), 12_123_000L),
new Asset(Uri.parse("http://2"), 22_123_000L),
new Asset(Uri.parse("http://3"), 32_122_999L),
new Asset(Uri.parse("http://4"), 42_123_000L))
.inOrder();
assertThat(parsingLoadable.getResult().stringAttributes)
.containsExactly(
new AssetListParser.StringAttribute("foo", "foo"),
new AssetListParser.StringAttribute("fooBar", "fooBar"),
new AssetListParser.StringAttribute("ASSETS", "stringValue"))
.inOrder();
}
@Test
public void load_withJsonArrayAsRoot_emptyResult() throws IOException {
byte[] assetListBytes = "[]".getBytes(Charset.defaultCharset());
ParsingLoadable<AssetListParser.AssetList> parsingLoadable =
new ParsingLoadable<>(
new ByteArrayDataSource(assetListBytes),
Uri.EMPTY,
C.DATA_TYPE_AD,
new AssetListParser());
parsingLoadable.load();
assertThat(parsingLoadable.getResult().assets).isEmpty();
assertThat(parsingLoadable.getResult().stringAttributes).isEmpty();
}
@Test
public void load_emptyInputStream_throwsEOFException() throws IOException {
ParsingLoadable<AssetListParser.AssetList> parsingLoadable =
new ParsingLoadable<>(
new ByteArrayDataSource(" ".getBytes(Charset.defaultCharset())),
Uri.EMPTY,
C.DATA_TYPE_AD,
new AssetListParser());
assertThrows(EOFException.class, parsingLoadable::load);
}
}

View File

@ -0,0 +1,112 @@
{
"bar": [
{},
{
"foo": [
{
"URI": "http://1",
"DURATION": 12.123
},
{
"URI": "http://2",
"DURATION": 22.123
},
{
"URI": "http://3",
"DURATION": 32.123
}
]
}
],
"foo": "foo",
"fooObject": {
"attr1": "",
"attr2": 2.0
},
"ASSETS": [
{
"URI": "http://1",
"DURATION": 12.123,
"foo": 12,
"fooBar": "text",
"booleanFoo": true,
"fooObject": {
"id": 23
},
"fooEmpty": {},
"fooArray": [
{
"id": "1"
},
{
"id": "2"
}
]
},
{
"URI": null,
"DURATION": 12.123
},
{
"URI": 1.2,
"DURATION": 12.123
},
{},
{
"URI": true,
"DURATION": 12.123
},
{
"DURATION": 12.123
},
{
"URI": "http://2",
"DURATION": 22.123
},
{
"URI": "http://2",
"DURATION": null
},
{
"URI": "http://2",
"DURATION": "12.0"
},
{
"URI": "http://2",
"DURATION": "twelve"
},
{
"URI": "http://2",
"DURATION": false
},
{
"URI": "http://2"
},
{
"foo": 12,
"fooBar": "text",
"fooObject": {
"id": 23
},
"fooEmpty": {},
"fooArray": [
{
"id": "1"
}
],
"URI": "http://3",
"DURATION": 32.123
}
],
"fooBar": "fooBar",
"barFoo": 12.00,
"nullFoo": null,
"ASSETS": "stringValue",
"ASSETS": [
{
"URI": "http://4",
"DURATION": 42.123
}
],
"booleanFoo": true
}