ScriptTagPayloadReader.java

/*
 * Copyright (C) 2016 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.flv;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.extractor.DummyTrackOutput;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Parses Script Data tags from an FLV stream and extracts metadata information. */
/* package */ final class ScriptTagPayloadReader extends TagPayloadReader {

  private static final String NAME_METADATA = "onMetaData";
  private static final String KEY_DURATION = "duration";
  private static final String KEY_KEY_FRAMES = "keyframes";
  private static final String KEY_FILE_POSITIONS = "filepositions";
  private static final String KEY_TIMES = "times";

  // AMF object types
  private static final int AMF_TYPE_NUMBER = 0;
  private static final int AMF_TYPE_BOOLEAN = 1;
  private static final int AMF_TYPE_STRING = 2;
  private static final int AMF_TYPE_OBJECT = 3;
  private static final int AMF_TYPE_ECMA_ARRAY = 8;
  private static final int AMF_TYPE_END_MARKER = 9;
  private static final int AMF_TYPE_STRICT_ARRAY = 10;
  private static final int AMF_TYPE_DATE = 11;

  private long durationUs;
  private long[] keyFrameTimesUs;
  private long[] keyFrameTagPositions;

  public ScriptTagPayloadReader() {
    super(new DummyTrackOutput());
    durationUs = C.TIME_UNSET;
    keyFrameTimesUs = new long[0];
    keyFrameTagPositions = new long[0];
  }

  public long getDurationUs() {
    return durationUs;
  }

  public long[] getKeyFrameTimesUs() {
    return keyFrameTimesUs;
  }

  public long[] getKeyFrameTagPositions() {
    return keyFrameTagPositions;
  }

  @Override
  public void seek() {
    // Do nothing.
  }

  @Override
  protected boolean parseHeader(ParsableByteArray data) {
    return true;
  }

  @Override
  protected boolean parsePayload(ParsableByteArray data, long timeUs) {
    int nameType = readAmfType(data);
    if (nameType != AMF_TYPE_STRING) {
      // Ignore segments with unexpected name type.
      return false;
    }
    String name = readAmfString(data);
    if (!NAME_METADATA.equals(name)) {
      // We're only interested in metadata.
      return false;
    }
    if (data.bytesLeft() == 0) {
      // The metadata script tag has no value.
      return false;
    }
    int type = readAmfType(data);
    if (type != AMF_TYPE_ECMA_ARRAY) {
      // We're not interested in this metadata.
      return false;
    }
    Map<String, Object> metadata = readAmfEcmaArray(data);
    // Set the duration to the value contained in the metadata, if present.
    @Nullable Object durationSecondsObj = metadata.get(KEY_DURATION);
    if (durationSecondsObj instanceof Double) {
      double durationSeconds = (double) durationSecondsObj;
      if (durationSeconds > 0.0) {
        durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
      }
    }
    // Set the key frame times and positions to the value contained in the metadata, if present.
    @Nullable Object keyFramesObj = metadata.get(KEY_KEY_FRAMES);
    if (keyFramesObj instanceof Map) {
      Map<?, ?> keyFrames = (Map<?, ?>) keyFramesObj;
      @Nullable Object positionsObj = keyFrames.get(KEY_FILE_POSITIONS);
      @Nullable Object timesSecondsObj = keyFrames.get(KEY_TIMES);
      if (positionsObj instanceof List && timesSecondsObj instanceof List) {
        List<?> positions = (List<?>) positionsObj;
        List<?> timesSeconds = (List<?>) timesSecondsObj;
        int keyFrameCount = timesSeconds.size();
        keyFrameTimesUs = new long[keyFrameCount];
        keyFrameTagPositions = new long[keyFrameCount];
        for (int i = 0; i < keyFrameCount; i++) {
          Object positionObj = positions.get(i);
          Object timeSecondsObj = timesSeconds.get(i);
          if (timeSecondsObj instanceof Double && positionObj instanceof Double) {
            keyFrameTimesUs[i] = (long) (((Double) timeSecondsObj) * C.MICROS_PER_SECOND);
            keyFrameTagPositions[i] = ((Double) positionObj).longValue();
          } else {
            keyFrameTimesUs = new long[0];
            keyFrameTagPositions = new long[0];
            break;
          }
        }
      }
    }
    return false;
  }

  private static int readAmfType(ParsableByteArray data) {
    return data.readUnsignedByte();
  }

  /**
   * Read a boolean from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static Boolean readAmfBoolean(ParsableByteArray data) {
    return data.readUnsignedByte() == 1;
  }

  /**
   * Read a double number from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static Double readAmfDouble(ParsableByteArray data) {
    return Double.longBitsToDouble(data.readLong());
  }

  /**
   * Read a string from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static String readAmfString(ParsableByteArray data) {
    int size = data.readUnsignedShort();
    int position = data.getPosition();
    data.skipBytes(size);
    return new String(data.getData(), position, size);
  }

  /**
   * Read an array from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
    int count = data.readUnsignedIntToInt();
    ArrayList<Object> list = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
      int type = readAmfType(data);
      Object value = readAmfData(data, type);
      if (value != null) {
        list.add(value);
      }
    }
    return list;
  }

  /**
   * Read an object from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
    HashMap<String, Object> array = new HashMap<>();
    while (true) {
      String key = readAmfString(data);
      int type = readAmfType(data);
      if (type == AMF_TYPE_END_MARKER) {
        break;
      }
      Object value = readAmfData(data, type);
      if (value != null) {
        array.put(key, value);
      }
    }
    return array;
  }

  /**
   * Read an ECMA array from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
    int count = data.readUnsignedIntToInt();
    HashMap<String, Object> array = new HashMap<>(count);
    for (int i = 0; i < count; i++) {
      String key = readAmfString(data);
      int type = readAmfType(data);
      Object value = readAmfData(data, type);
      if (value != null) {
        array.put(key, value);
      }
    }
    return array;
  }

  /**
   * Read a date from an AMF encoded buffer.
   *
   * @param data The buffer from which to read.
   * @return The value read from the buffer.
   */
  private static Date readAmfDate(ParsableByteArray data) {
    Date date = new Date((long) readAmfDouble(data).doubleValue());
    data.skipBytes(2); // Skip reserved bytes.
    return date;
  }

  @Nullable
  private static Object readAmfData(ParsableByteArray data, int type) {
    switch (type) {
      case AMF_TYPE_NUMBER:
        return readAmfDouble(data);
      case AMF_TYPE_BOOLEAN:
        return readAmfBoolean(data);
      case AMF_TYPE_STRING:
        return readAmfString(data);
      case AMF_TYPE_OBJECT:
        return readAmfObject(data);
      case AMF_TYPE_ECMA_ARRAY:
        return readAmfEcmaArray(data);
      case AMF_TYPE_STRICT_ARRAY:
        return readAmfStrictArray(data);
      case AMF_TYPE_DATE:
        return readAmfDate(data);
      default:
        // We don't log a warning because there are types that we knowingly don't support.
        return null;
    }
  }
}