/** * Creates a {@link SeekMap} wrapper for this FlacSeekTable. * * @param firstFrameOffset Offset of the first FLAC frame * @param sampleRate Sample rate of the FLAC file. * @return A SeekMap wrapper for this FlacSeekTable. */ public SeekMap createSeekMap(final long firstFrameOffset, final long sampleRate) { return new SeekMap() { @Override public boolean isSeekable() { return true; } @Override public long getPosition(long timeUs) { long sample = (timeUs * sampleRate) / 1000000L; int index = Util.binarySearchFloor(sampleNumbers, sample, true, true); return firstFrameOffset + offsets[index]; } }; }
private static void assertSeekMap(SeekMap seekMap, boolean haveStss) { assertNotNull(seekMap); int expectedSeekPosition = getSampleOffset(0); for (int i = 0; i < SAMPLE_TIMESTAMPS.length; i++) { // Seek to just before the current sample. long seekPositionUs = getVideoTimestampUs(SAMPLE_TIMESTAMPS[i]) - 1; assertEquals(expectedSeekPosition, seekMap.getPosition(seekPositionUs)); // If the current sample is a sync sample, the expected seek position will change. if (SAMPLE_IS_SYNC[i] || !haveStss) { expectedSeekPosition = getSampleOffset(i); } // Seek to the current sample. seekPositionUs = getVideoTimestampUs(SAMPLE_TIMESTAMPS[i]); assertEquals(expectedSeekPosition, seekMap.getPosition(seekPositionUs)); // Seek to just after the current sample. seekPositionUs = getVideoTimestampUs(SAMPLE_TIMESTAMPS[i]) + 1; assertEquals(expectedSeekPosition, seekMap.getPosition(seekPositionUs)); } }
/** * Builds a {@link SeekMap} from the recently gathered Cues information. * * @return The built {@link SeekMap}. May be {@link SeekMap#UNSEEKABLE} if cues information was * missing or incomplete. */ private SeekMap buildSeekMap() { if (segmentContentPosition == UNKNOWN || durationUs == C.UNKNOWN_TIME_US || cueTimesUs == null || cueTimesUs.size() == 0 || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) { // Cues information is missing or incomplete. cueTimesUs = null; cueClusterPositions = null; return SeekMap.UNSEEKABLE; } int cuePointsSize = cueTimesUs.size(); int[] sizes = new int[cuePointsSize]; long[] offsets = new long[cuePointsSize]; long[] durationsUs = new long[cuePointsSize]; long[] timesUs = new long[cuePointsSize]; for (int i = 0; i < cuePointsSize; i++) { timesUs[i] = cueTimesUs.get(i); offsets[i] = segmentContentPosition + cueClusterPositions.get(i); } for (int i = 0; i < cuePointsSize - 1; i++) { sizes[i] = (int) (offsets[i + 1] - offsets[i]); durationsUs[i] = timesUs[i + 1] - timesUs[i]; } sizes[cuePointsSize - 1] = (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; cueTimesUs = null; cueClusterPositions = null; return new ChunkIndex(sizes, offsets, durationsUs, timesUs); }
@Override public void init(ExtractorOutput output) { adtsReader = new AdtsReader(output.track(0)); output.endTracks(); output.seekMap(SeekMap.UNSEEKABLE); }
@Override public void init(ExtractorOutput output) { this.output = output; output.seekMap(SeekMap.UNSEEKABLE); }
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { if (atomHeaderBytesRead == 0) { // Read the standard length atom header. if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { return false; } atomHeaderBytesRead = Atom.HEADER_SIZE; atomHeader.setPosition(0); atomSize = atomHeader.readUnsignedInt(); atomType = atomHeader.readInt(); } if (atomSize == Atom.LONG_SIZE_PREFIX) { // Read the extended atom size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); } if (atomType == Atom.TYPE_mdat) { if (!haveOutputSeekMap) { extractorOutput.seekMap(SeekMap.UNSEEKABLE); haveOutputSeekMap = true; } if (fragmentRun.sampleEncryptionDataNeedsFill) { parserState = STATE_READING_ENCRYPTION_DATA; } else { parserState = STATE_READING_SAMPLE_START; } return true; } if (shouldParseAtom(atomType)) { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; containerAtoms.add(new ContainerAtom(atomType, endPosition)); enterReadingAtomHeaderState(); } else { // We don't support parsing of leaf atoms that define extended atom sizes, or that have // lengths greater than Integer.MAX_VALUE. Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE); Assertions.checkState(atomSize <= Integer.MAX_VALUE); atomData = new ParsableByteArray((int) atomSize); System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); parserState = STATE_READING_ATOM_PAYLOAD; } } else { // We don't support skipping of atoms that have lengths greater than Integer.MAX_VALUE. Assertions.checkState(atomSize <= Integer.MAX_VALUE); atomData = null; parserState = STATE_READING_ATOM_PAYLOAD; } return true; }
void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { switch (id) { case ID_SEGMENT: if (segmentContentPosition != UNKNOWN && segmentContentPosition != contentPosition) { throw new ParserException("Multiple Segment elements not supported"); } segmentContentPosition = contentPosition; segmentContentSize = contentSize; return; case ID_SEEK: seekEntryId = UNKNOWN; seekEntryPosition = UNKNOWN; return; case ID_CUES: cueTimesUs = new LongArray(); cueClusterPositions = new LongArray(); return; case ID_CUE_POINT: seenClusterPositionForCurrentCuePoint = false; return; case ID_CLUSTER: if (cuesState == CUES_STATE_NOT_BUILT) { // We need to build cues before parsing the cluster. if (cuesContentPosition != UNKNOWN) { // We know where the Cues element is located. Seek to request it. seekForCues = true; } else { // We don't know where the Cues element is located. It's most likely omitted. Allow // playback, but disable seeking. extractorOutput.seekMap(SeekMap.UNSEEKABLE); cuesState = CUES_STATE_BUILT; } } return; case ID_BLOCK_GROUP: sampleSeenReferenceBlock = false; return; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. return; case ID_CONTENT_ENCRYPTION: trackFormat.hasContentEncryption = true; return; case ID_TRACK_ENTRY: trackFormat = new TrackFormat(); return; default: return; } }
@Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { decoder.setData(input); if (!metadataParsed) { FlacStreamInfo streamInfo = decoder.decodeMetadata(); if (streamInfo == null) { throw new IOException("Metadata decoding failed"); } metadataParsed = true; output.seekMap(new SeekMap() { final boolean isSeekable = decoder.getSeekPosition(0) != -1; @Override public boolean isSeekable() { return isSeekable; } @Override public long getPosition(long timeUs) { return isSeekable ? decoder.getSeekPosition(timeUs) : 0; } }); MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_RAW, streamInfo.bitRate(), MediaFormat.NO_VALUE, streamInfo.durationUs(), streamInfo.channels, streamInfo.sampleRate, null, null, C.ENCODING_PCM_16BIT); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); } outputBuffer.reset(); int size = decoder.decodeSample(outputByteBuffer); if (size <= 0) { return RESULT_END_OF_INPUT; } trackOutput.sampleData(outputBuffer, size); trackOutput.sampleMetadata(decoder.getLastSampleTimestamp(), C.SAMPLE_FLAG_SYNC, size, 0, null); return decoder.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; }
@Override public void init(ExtractorOutput output) { adtsReader = new AdtsReader(output.track(0), output.track(1)); output.endTracks(); output.seekMap(SeekMap.UNSEEKABLE); }
@Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { long position = input.getPosition(); if (!oggParser.readPacket(input, scratch)) { return Extractor.RESULT_END_OF_INPUT; } byte[] data = scratch.data; if (streamInfo == null) { streamInfo = new FlacStreamInfo(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, scratch.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List<byte[]> initializationData = Collections.singletonList(metadata); MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_FLAC, streamInfo.bitRate(), MediaFormat.NO_VALUE, streamInfo.durationUs(), streamInfo.channels, streamInfo.sampleRate, initializationData, null); trackOutput.format(mediaFormat); } else if (data[0] == AUDIO_PACKET_TYPE) { if (!firstAudioPacketProcessed) { if (seekTable != null) { extractorOutput.seekMap(seekTable.createSeekMap(position, streamInfo.sampleRate)); seekTable = null; } else { extractorOutput.seekMap(SeekMap.UNSEEKABLE); } firstAudioPacketProcessed = true; } trackOutput.sampleData(scratch, scratch.limit()); scratch.setPosition(0); long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch); trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) { seekTable = FlacSeekTable.parseSeekTable(scratch); } scratch.reset(); return Extractor.RESULT_CONTINUE; }
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { if (atomHeaderBytesRead == 0) { // Read the standard length atom header. if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { return false; } atomHeaderBytesRead = Atom.HEADER_SIZE; atomHeader.setPosition(0); atomSize = atomHeader.readUnsignedInt(); atomType = atomHeader.readInt(); } if (atomSize == Atom.LONG_SIZE_PREFIX) { // Read the extended atom size. int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); atomHeaderBytesRead += headerBytesRemaining; atomSize = atomHeader.readUnsignedLongToLong(); } long atomPosition = input.getPosition() - atomHeaderBytesRead; if (atomType == Atom.TYPE_moof) { // The data positions may be updated when parsing the tfhd/trun. int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { TrackFragment fragment = trackBundles.valueAt(i).fragment; fragment.auxiliaryDataPosition = atomPosition; fragment.dataPosition = atomPosition; } } if (atomType == Atom.TYPE_mdat) { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { extractorOutput.seekMap(SeekMap.UNSEEKABLE); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; return true; } if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; containerAtoms.add(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { // Start reading the first child atom. enterReadingAtomHeaderState(); } } else if (shouldParseLeafAtom(atomType)) { if (atomHeaderBytesRead != Atom.HEADER_SIZE) { throw new ParserException("Leaf atom defines extended atom size (unsupported)."); } if (atomSize > Integer.MAX_VALUE) { throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); } atomData = new ParsableByteArray((int) atomSize); System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); parserState = STATE_READING_ATOM_PAYLOAD; } else { if (atomSize > Integer.MAX_VALUE) { throw new ParserException("Skipping atom with length > 2147483647 (unsupported)."); } atomData = null; parserState = STATE_READING_ATOM_PAYLOAD; } return true; }
void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { switch (id) { case ID_SEGMENT: if (segmentContentPosition != UNKNOWN && segmentContentPosition != contentPosition) { throw new ParserException("Multiple Segment elements not supported"); } segmentContentPosition = contentPosition; segmentContentSize = contentSize; return; case ID_SEEK: seekEntryId = UNKNOWN; seekEntryPosition = UNKNOWN; return; case ID_CUES: cueTimesUs = new LongArray(); cueClusterPositions = new LongArray(); return; case ID_CUE_POINT: seenClusterPositionForCurrentCuePoint = false; return; case ID_CLUSTER: if (!sentSeekMap) { // We need to build cues before parsing the cluster. if (cuesContentPosition != UNKNOWN) { // We know where the Cues element is located. Seek to request it. seekForCues = true; } else { // We don't know where the Cues element is located. It's most likely omitted. Allow // playback, but disable seeking. extractorOutput.seekMap(SeekMap.UNSEEKABLE); sentSeekMap = true; } } return; case ID_BLOCK_GROUP: sampleSeenReferenceBlock = false; return; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. return; case ID_CONTENT_ENCRYPTION: currentTrack.hasContentEncryption = true; return; case ID_TRACK_ENTRY: currentTrack = new Track(); return; default: return; } }
private void assertIndexUnseekable() { assertEquals(SeekMap.UNSEEKABLE, extractorOutput.seekMap); }
/** * Returns a {@link SeekMap} parsed from the chunk, or null. * <p> * Should be called after loading has completed. */ public SeekMap getSeekMap() { return seekMap; }