| /* |
| * 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; |
| } |
| } |
| } |