IcyHeaders.java

/*
 * 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 androidx.media3.extractor.metadata.icy;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.List;
import java.util.Map;

/** ICY headers. */
@UnstableApi
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 void populateMediaMetadata(MediaMetadata.Builder builder) {
    if (name != null) {
      builder.setStation(name);
    }
    if (genre != null) {
      builder.setGenre(genre);
    }
  }

  @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];
        }
      };
}