mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add JSON asset list parser
PiperOrigin-RevId: 697966021
This commit is contained in:
parent
98723dc0a8
commit
b9c9d95b90
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user