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