blob: 42bb1e019e113d453dbf8ce12a671d70fe94502f [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.extractor.flv;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
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;
}
}
}