blob: 996ae2f69b4657d967e03d89ac639c0ba5a66252 [file] [log] [blame]
/*
* Copyright (C) 2019 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.ts;
import static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD;
import static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH;
import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.Ac4Util;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
/** Extracts data from AC-4 bitstreams. */
public final class Ac4Extractor implements Extractor {
/** Factory for {@link Ac4Extractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Ac4Extractor()};
/**
* The maximum number of bytes to search when sniffing, excluding ID3 information, before giving
* up.
*/
private static final int MAX_SNIFF_BYTES = 8 * 1024;
/**
* The size of the reading buffer, in bytes. This value is determined based on the maximum frame
* size used in broadcast applications.
*/
private static final int READ_BUFFER_SIZE = 16384;
/** The size of the frame header, in bytes. */
private static final int FRAME_HEADER_SIZE = 7;
private final Ac4Reader reader;
private final ParsableByteArray sampleData;
private boolean startedPacket;
/** Creates a new extractor for AC-4 bitstreams. */
public Ac4Extractor() {
reader = new Ac4Reader();
sampleData = new ParsableByteArray(READ_BUFFER_SIZE);
}
// Extractor implementation.
@Override
public boolean sniff(ExtractorInput input) throws IOException {
// Skip any ID3 headers.
ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH);
int startPosition = 0;
while (true) {
input.peekFully(scratch.getData(), /* offset= */ 0, ID3_HEADER_LENGTH);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != ID3_TAG) {
break;
}
scratch.skipBytes(3); // version, flags
int length = scratch.readSynchSafeInt();
startPosition += 10 + length;
input.advancePeekPosition(length);
}
input.resetPeekPosition();
input.advancePeekPosition(startPosition);
int headerPosition = startPosition;
int validFramesCount = 0;
while (true) {
input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ FRAME_HEADER_SIZE);
scratch.setPosition(0);
int syncBytes = scratch.readUnsignedShort();
if (syncBytes != AC40_SYNCWORD && syncBytes != AC41_SYNCWORD) {
validFramesCount = 0;
input.resetPeekPosition();
if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
return false;
}
input.advancePeekPosition(headerPosition);
} else {
if (++validFramesCount >= 4) {
return true;
}
int frameSize = Ac4Util.parseAc4SyncframeSize(scratch.getData(), syncBytes);
if (frameSize == C.LENGTH_UNSET) {
return false;
}
input.advancePeekPosition(frameSize - FRAME_HEADER_SIZE);
}
}
}
@Override
public void init(ExtractorOutput output) {
reader.createTracks(
output, new TrackIdGenerator(/* firstTrackId= */ 0, /* trackIdIncrement= */ 1));
output.endTracks();
output.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));
}
@Override
public void seek(long position, long timeUs) {
startedPacket = false;
reader.seek();
}
@Override
public void release() {
// Do nothing.
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
int bytesRead =
input.read(sampleData.getData(), /* offset= */ 0, /* length= */ READ_BUFFER_SIZE);
if (bytesRead == C.RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT;
}
// Feed whatever data we have to the reader, regardless of whether the read finished or not.
sampleData.setPosition(0);
sampleData.setLimit(bytesRead);
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
// unnecessary to copy the data through packetBuffer.
reader.consume(sampleData);
return RESULT_CONTINUE;
}
}