blob: 36586e70595d8a67ca71b53ecaac9297c2414133 [file] [log] [blame]
/*
* Copyright 2020 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.mp4;
import static com.google.android.exoplayer2.extractor.Extractor.RESULT_SEEK;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Splitter;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Reads Samsung Extension Format (SEF) metadata.
*
* <p>To be used in conjunction with {@link Mp4Extractor}.
*/
/* package */ final class SefReader {
/** Reader states. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_SHOULD_CHECK_FOR_SEF,
STATE_CHECKING_FOR_SEF,
STATE_READING_SDRS,
STATE_READING_SEF_DATA
})
private @interface State {}
private static final int STATE_SHOULD_CHECK_FOR_SEF = 0;
private static final int STATE_CHECKING_FOR_SEF = 1;
private static final int STATE_READING_SDRS = 2;
private static final int STATE_READING_SEF_DATA = 3;
/** Supported data types. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TYPE_SLOW_MOTION_DATA,
TYPE_SUPER_SLOW_MOTION_DATA,
TYPE_SUPER_SLOW_MOTION_BGM,
TYPE_SUPER_SLOW_MOTION_EDIT_DATA,
TYPE_SUPER_SLOW_DEFLICKERING_ON
})
private @interface DataType {}
private static final int TYPE_SLOW_MOTION_DATA = 0x0890; // 2192
private static final int TYPE_SUPER_SLOW_MOTION_DATA = 0x0b00; // 2816
private static final int TYPE_SUPER_SLOW_MOTION_BGM = 0x0b01; // 2817
private static final int TYPE_SUPER_SLOW_MOTION_EDIT_DATA = 0x0b03; // 2819
private static final int TYPE_SUPER_SLOW_DEFLICKERING_ON = 0x0b04; // 2820
private static final String TAG = "SefReader";
/**
* Hex representation of `SEFT` (in ASCII).
*
* <p>This is the last 4 bytes of a file that has Samsung Extension Format (SEF) data.
*/
private static final int SAMSUNG_TAIL_SIGNATURE = 0x53454654;
/** Start signature (4 bytes), SEF version (4 bytes), SDR count (4 bytes). */
private static final int TAIL_HEADER_LENGTH = 12;
/** Tail offset (4 bytes), tail signature (4 bytes). */
private static final int TAIL_FOOTER_LENGTH = 8;
private static final int LENGTH_OF_ONE_SDR = 12;
private static final Splitter COLON_SPLITTER = Splitter.on(':');
private static final Splitter ASTERISK_SPLITTER = Splitter.on('*');
private final List<DataReference> dataReferences;
@State private int readerState;
private int tailLength;
public SefReader() {
dataReferences = new ArrayList<>();
readerState = STATE_SHOULD_CHECK_FOR_SEF;
}
public void reset() {
dataReferences.clear();
readerState = STATE_SHOULD_CHECK_FOR_SEF;
}
@Extractor.ReadResult
public int read(
ExtractorInput input,
PositionHolder seekPosition,
List<Metadata.Entry> slowMotionMetadataEntries)
throws IOException {
switch (readerState) {
case STATE_SHOULD_CHECK_FOR_SEF:
long inputLength = input.getLength();
seekPosition.position =
inputLength == C.LENGTH_UNSET || inputLength < TAIL_FOOTER_LENGTH
? 0
: inputLength - TAIL_FOOTER_LENGTH;
readerState = STATE_CHECKING_FOR_SEF;
break;
case STATE_CHECKING_FOR_SEF:
checkForSefData(input, seekPosition);
break;
case STATE_READING_SDRS:
readSdrs(input, seekPosition);
break;
case STATE_READING_SEF_DATA:
readSefData(input, slowMotionMetadataEntries);
seekPosition.position = 0;
break;
default:
throw new IllegalStateException();
}
return RESULT_SEEK;
}
private void checkForSefData(ExtractorInput input, PositionHolder seekPosition)
throws IOException {
ParsableByteArray scratch = new ParsableByteArray(/* limit= */ TAIL_FOOTER_LENGTH);
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ TAIL_FOOTER_LENGTH);
tailLength = scratch.readLittleEndianInt() + TAIL_FOOTER_LENGTH;
if (scratch.readInt() != SAMSUNG_TAIL_SIGNATURE) {
seekPosition.position = 0;
return;
}
// input.getPosition is at the very end of the tail, so jump forward by tailLength, but
// account for the tail header, which needs to be ignored.
seekPosition.position = input.getPosition() - (tailLength - TAIL_HEADER_LENGTH);
readerState = STATE_READING_SDRS;
}
private void readSdrs(ExtractorInput input, PositionHolder seekPosition) throws IOException {
long streamLength = input.getLength();
int sdrsLength = tailLength - TAIL_HEADER_LENGTH - TAIL_FOOTER_LENGTH;
ParsableByteArray scratch = new ParsableByteArray(/* limit= */ sdrsLength);
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ sdrsLength);
for (int i = 0; i < sdrsLength / LENGTH_OF_ONE_SDR; i++) {
scratch.skipBytes(2); // SDR data sub info flag and reserved bits (2).
@DataType int dataType = scratch.readLittleEndianShort();
switch (dataType) {
case TYPE_SLOW_MOTION_DATA:
case TYPE_SUPER_SLOW_MOTION_DATA:
case TYPE_SUPER_SLOW_MOTION_BGM:
case TYPE_SUPER_SLOW_MOTION_EDIT_DATA:
case TYPE_SUPER_SLOW_DEFLICKERING_ON:
// The read int is the distance from the tail info to the start of the metadata.
// Calculated as an offset from the start by working backwards.
long startOffset = streamLength - tailLength - scratch.readLittleEndianInt();
int size = scratch.readLittleEndianInt();
dataReferences.add(new DataReference(dataType, startOffset, size));
break;
default:
scratch.skipBytes(8); // startPosition (4), size (4).
}
}
if (dataReferences.isEmpty()) {
seekPosition.position = 0;
return;
}
readerState = STATE_READING_SEF_DATA;
seekPosition.position = dataReferences.get(0).startOffset;
}
private void readSefData(ExtractorInput input, List<Metadata.Entry> slowMotionMetadataEntries)
throws IOException {
long dataStartOffset = input.getPosition();
int totalDataLength = (int) (input.getLength() - input.getPosition() - tailLength);
ParsableByteArray data = new ParsableByteArray(/* limit= */ totalDataLength);
input.readFully(data.getData(), 0, totalDataLength);
for (int i = 0; i < dataReferences.size(); i++) {
DataReference dataReference = dataReferences.get(i);
int intendedPosition = (int) (dataReference.startOffset - dataStartOffset);
data.setPosition(intendedPosition);
// The data type is derived from the name because the SEF format has inconsistent data type
// values.
data.skipBytes(4); // data type (2), data sub info (2).
int nameLength = data.readLittleEndianInt();
String name = data.readString(nameLength);
@DataType int dataType = nameToDataType(name);
int remainingDataLength = dataReference.size - (8 + nameLength);
switch (dataType) {
case TYPE_SLOW_MOTION_DATA:
slowMotionMetadataEntries.add(readSlowMotionData(data, remainingDataLength));
break;
case TYPE_SUPER_SLOW_MOTION_DATA:
case TYPE_SUPER_SLOW_MOTION_BGM:
case TYPE_SUPER_SLOW_MOTION_EDIT_DATA:
case TYPE_SUPER_SLOW_DEFLICKERING_ON:
break;
default:
throw new IllegalStateException();
}
}
}
private static SlowMotionData readSlowMotionData(ParsableByteArray data, int dataLength)
throws ParserException {
List<SlowMotionData.Segment> segments = new ArrayList<>();
String dataString = data.readString(dataLength);
List<String> segmentStrings = ASTERISK_SPLITTER.splitToList(dataString);
for (int i = 0; i < segmentStrings.size(); i++) {
List<String> values = COLON_SPLITTER.splitToList(segmentStrings.get(i));
if (values.size() != 3) {
throw ParserException.createForMalformedContainer(/* message= */ null, /* cause= */ null);
}
try {
long startTimeMs = Long.parseLong(values.get(0));
long endTimeMs = Long.parseLong(values.get(1));
int speedMode = Integer.parseInt(values.get(2));
int speedDivisor = 1 << (speedMode - 1);
segments.add(new SlowMotionData.Segment(startTimeMs, endTimeMs, speedDivisor));
} catch (NumberFormatException e) {
throw ParserException.createForMalformedContainer(/* message= */ null, /* cause= */ e);
}
}
return new SlowMotionData(segments);
}
@DataType
private static int nameToDataType(String name) throws ParserException {
switch (name) {
case "SlowMotion_Data":
return TYPE_SLOW_MOTION_DATA;
case "Super_SlowMotion_Data":
return TYPE_SUPER_SLOW_MOTION_DATA;
case "Super_SlowMotion_BGM":
return TYPE_SUPER_SLOW_MOTION_BGM;
case "Super_SlowMotion_Edit_Data":
return TYPE_SUPER_SLOW_MOTION_EDIT_DATA;
case "Super_SlowMotion_Deflickering_On":
return TYPE_SUPER_SLOW_DEFLICKERING_ON;
default:
throw ParserException.createForMalformedContainer("Invalid SEF name", /* cause= */ null);
}
}
private static final class DataReference {
@DataType public final int dataType;
public final long startOffset;
public final int size;
public DataReference(@DataType int dataType, long startOffset, int size) {
this.dataType = dataType;
this.startOffset = startOffset;
this.size = size;
}
}
}