summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java243
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java107
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java19
4 files changed, 470 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
new file mode 100644
index 0000000000..1d44219eda
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Decodes ICY stream information. */
+public final class IcyDecoder implements MetadataDecoder {
+
+ private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL);
+ private static final String STREAM_KEY_NAME = "streamtitle";
+ private static final String STREAM_KEY_URL = "streamurl";
+
+ private final CharsetDecoder utf8Decoder;
+ private final CharsetDecoder iso88591Decoder;
+
+ public IcyDecoder() {
+ utf8Decoder = Charset.forName(C.UTF8_NAME).newDecoder();
+ iso88591Decoder = Charset.forName(C.ISO88591_NAME).newDecoder();
+ }
+
+ @Override
+ @SuppressWarnings("ByteBufferBackingArray")
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ @Nullable String icyString = decodeToString(buffer);
+ byte[] icyBytes = new byte[buffer.limit()];
+ buffer.get(icyBytes);
+
+ if (icyString == null) {
+ return new Metadata(new IcyInfo(icyBytes, /* title= */ null, /* url= */ null));
+ }
+
+ @Nullable String name = null;
+ @Nullable String url = null;
+ int index = 0;
+ Matcher matcher = METADATA_ELEMENT.matcher(icyString);
+ while (matcher.find(index)) {
+ @Nullable String key = Util.toLowerInvariant(matcher.group(1));
+ @Nullable String value = matcher.group(2);
+ switch (key) {
+ case STREAM_KEY_NAME:
+ name = value;
+ break;
+ case STREAM_KEY_URL:
+ url = value;
+ break;
+ }
+ index = matcher.end();
+ }
+ return new Metadata(new IcyInfo(icyBytes, name, url));
+ }
+
+ // The ICY spec doesn't specify a character encoding, and there's no way to communicate one
+ // either. So try decoding UTF-8 first, then fall back to ISO-8859-1.
+ // https://github.com/google/ExoPlayer/issues/6753
+ @Nullable
+ private String decodeToString(ByteBuffer data) {
+ try {
+ return utf8Decoder.decode(data).toString();
+ } catch (CharacterCodingException e) {
+ // Fall through to try ISO-8859-1 decoding.
+ } finally {
+ utf8Decoder.reset();
+ data.rewind();
+ }
+ try {
+ return iso88591Decoder.decode(data).toString();
+ } catch (CharacterCodingException e) {
+ return null;
+ } finally {
+ iso88591Decoder.reset();
+ data.rewind();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
new file mode 100644
index 0000000000..638e7594eb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.List;
+import java.util.Map;
+
+/** ICY headers. */
+public final class IcyHeaders implements Metadata.Entry {
+
+ public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData";
+ public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1";
+
+ private static final String TAG = "IcyHeaders";
+
+ private static final String RESPONSE_HEADER_BITRATE = "icy-br";
+ private static final String RESPONSE_HEADER_GENRE = "icy-genre";
+ private static final String RESPONSE_HEADER_NAME = "icy-name";
+ private static final String RESPONSE_HEADER_URL = "icy-url";
+ private static final String RESPONSE_HEADER_PUB = "icy-pub";
+ private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint";
+
+ /**
+ * Parses {@link IcyHeaders} from response headers.
+ *
+ * @param responseHeaders The response headers.
+ * @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.
+ */
+ @Nullable
+ public static IcyHeaders parse(Map<String, List<String>> responseHeaders) {
+ boolean icyHeadersPresent = false;
+ int bitrate = Format.NO_VALUE;
+ String genre = null;
+ String name = null;
+ String url = null;
+ boolean isPublic = false;
+ int metadataInterval = C.LENGTH_UNSET;
+
+ List<String> headers = responseHeaders.get(RESPONSE_HEADER_BITRATE);
+ if (headers != null) {
+ String bitrateHeader = headers.get(0);
+ try {
+ bitrate = Integer.parseInt(bitrateHeader) * 1000;
+ if (bitrate > 0) {
+ icyHeadersPresent = true;
+ } else {
+ Log.w(TAG, "Invalid bitrate: " + bitrateHeader);
+ bitrate = Format.NO_VALUE;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid bitrate header: " + bitrateHeader);
+ }
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_GENRE);
+ if (headers != null) {
+ genre = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_NAME);
+ if (headers != null) {
+ name = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_URL);
+ if (headers != null) {
+ url = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_PUB);
+ if (headers != null) {
+ isPublic = headers.get(0).equals("1");
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL);
+ if (headers != null) {
+ String metadataIntervalHeader = headers.get(0);
+ try {
+ metadataInterval = Integer.parseInt(metadataIntervalHeader);
+ if (metadataInterval > 0) {
+ icyHeadersPresent = true;
+ } else {
+ Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
+ metadataInterval = C.LENGTH_UNSET;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
+ }
+ }
+ return icyHeadersPresent
+ ? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval)
+ : null;
+ }
+
+ /**
+ * Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header
+ * was not present.
+ */
+ public final int bitrate;
+ /** The genre ({@code icy-genre}). */
+ @Nullable public final String genre;
+ /** The stream name ({@code icy-name}). */
+ @Nullable public final String name;
+ /** The URL of the radio station ({@code icy-url}). */
+ @Nullable public final String url;
+ /**
+ * Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not
+ * present.
+ */
+ public final boolean isPublic;
+
+ /**
+ * The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET}
+ * if the header was not present.
+ */
+ public final int metadataInterval;
+
+ /**
+ * @param bitrate See {@link #bitrate}.
+ * @param genre See {@link #genre}.
+ * @param name See {@link #name See}.
+ * @param url See {@link #url}.
+ * @param isPublic See {@link #isPublic}.
+ * @param metadataInterval See {@link #metadataInterval}.
+ */
+ public IcyHeaders(
+ int bitrate,
+ @Nullable String genre,
+ @Nullable String name,
+ @Nullable String url,
+ boolean isPublic,
+ int metadataInterval) {
+ Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0);
+ this.bitrate = bitrate;
+ this.genre = genre;
+ this.name = name;
+ this.url = url;
+ this.isPublic = isPublic;
+ this.metadataInterval = metadataInterval;
+ }
+
+ /* package */ IcyHeaders(Parcel in) {
+ bitrate = in.readInt();
+ genre = in.readString();
+ name = in.readString();
+ url = in.readString();
+ isPublic = Util.readBoolean(in);
+ metadataInterval = in.readInt();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ IcyHeaders other = (IcyHeaders) obj;
+ return bitrate == other.bitrate
+ && Util.areEqual(genre, other.genre)
+ && Util.areEqual(name, other.name)
+ && Util.areEqual(url, other.url)
+ && isPublic == other.isPublic
+ && metadataInterval == other.metadataInterval;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + bitrate;
+ result = 31 * result + (genre != null ? genre.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (url != null ? url.hashCode() : 0);
+ result = 31 * result + (isPublic ? 1 : 0);
+ result = 31 * result + metadataInterval;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "IcyHeaders: name=\""
+ + name
+ + "\", genre=\""
+ + genre
+ + "\", bitrate="
+ + bitrate
+ + ", metadataInterval="
+ + metadataInterval;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(bitrate);
+ dest.writeString(genre);
+ dest.writeString(name);
+ dest.writeString(url);
+ Util.writeBoolean(dest, isPublic);
+ dest.writeInt(metadataInterval);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<IcyHeaders> CREATOR =
+ new Parcelable.Creator<IcyHeaders>() {
+
+ @Override
+ public IcyHeaders createFromParcel(Parcel in) {
+ return new IcyHeaders(in);
+ }
+
+ @Override
+ public IcyHeaders[] newArray(int size) {
+ return new IcyHeaders[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
new file mode 100644
index 0000000000..4104e41c64
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/** ICY in-stream information. */
+public final class IcyInfo implements Metadata.Entry {
+
+ /** The complete metadata bytes used to construct this IcyInfo. */
+ public final byte[] rawMetadata;
+ /** The stream title if present and decodable, or {@code null}. */
+ @Nullable public final String title;
+ /** The stream URL if present and decodable, or {@code null}. */
+ @Nullable public final String url;
+
+ /**
+ * Construct a new IcyInfo from the source metadata, and optionally a StreamTitle and StreamUrl
+ * that have been extracted.
+ *
+ * @param rawMetadata See {@link #rawMetadata}.
+ * @param title See {@link #title}.
+ * @param url See {@link #url}.
+ */
+ public IcyInfo(byte[] rawMetadata, @Nullable String title, @Nullable String url) {
+ this.rawMetadata = rawMetadata;
+ this.title = title;
+ this.url = url;
+ }
+
+ /* package */ IcyInfo(Parcel in) {
+ rawMetadata = Assertions.checkNotNull(in.createByteArray());
+ title = in.readString();
+ url = in.readString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ IcyInfo other = (IcyInfo) obj;
+ // title & url are derived from rawMetadata, so no need to include them in the comparison.
+ return Arrays.equals(rawMetadata, other.rawMetadata);
+ }
+
+ @Override
+ public int hashCode() {
+ // title & url are derived from rawMetadata, so no need to include them in the hash.
+ return Arrays.hashCode(rawMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ICY: title=\"%s\", url=\"%s\", rawMetadata.length=\"%s\"", title, url, rawMetadata.length);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(rawMetadata);
+ dest.writeString(title);
+ dest.writeString(url);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<IcyInfo> CREATOR =
+ new Parcelable.Creator<IcyInfo>() {
+
+ @Override
+ public IcyInfo createFromParcel(Parcel in) {
+ return new IcyInfo(in);
+ }
+
+ @Override
+ public IcyInfo[] newArray(int size) {
+ return new IcyInfo[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java
new file mode 100644
index 0000000000..a8a45e2ef1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;