/** Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. */ private long maybeResynchronize(ExtractorInput extractorInput) throws IOException, InterruptedException { inputBuffer.mark(); if (!inputBuffer.readAllowingEndOfInput(extractorInput, scratch.data, 0, 4)) { return RESULT_END_OF_INPUT; } inputBuffer.returnToMark(); scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) { int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData); if (frameSize != -1) { MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); return RESULT_CONTINUE; } } synchronizedHeaderData = 0; inputBuffer.skip(extractorInput, 1); return synchronizeCatchingEndOfInput(extractorInput); }
/** * Sets {@link #seeker} to seek using metadata from {@link #inputBuffer}, which should have its * position set to the start of the first frame in the stream. On returning, * {@link #inputBuffer}'s position and mark will be set to the start of the first frame of audio. * * @param extractorInput Source of data for {@link #inputBuffer}. * @param headerPosition Position (byte offset) of the synchronized header in the stream. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already read during synchronization. * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already read during synchronization. */ private void setupSeeker(ExtractorInput extractorInput, long headerPosition) throws IOException, InterruptedException { // Try to set up seeking based on a XING or VBRI header. if (parseSeekerFrame(extractorInput, headerPosition, extractorInput.getLength())) { // Discard the parsed header so we start reading from the first audio frame. inputBuffer.mark(); if (seeker != null) { return; } // If there was a header but it was not usable, synchronize to the next frame so we don't // use an invalid bitrate for CBR seeking. This read is guaranteed to succeed if the frame was // already read during synchronization. inputBuffer.read(extractorInput, scratch.data, 0, 4); scratch.setPosition(0); headerPosition += synchronizedHeader.frameSize; MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); } inputBuffer.returnToMark(); seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000, extractorInput.getLength()); }
@Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) { return RESULT_END_OF_INPUT; } if (seeker == null) { setupSeeker(input); extractorOutput.seekMap(seeker); MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, synchronizedHeader.mimeType, MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, seeker.getDurationUs(), synchronizedHeader.channels, synchronizedHeader.sampleRate, null, null); if (gaplessInfo != null) { mediaFormat = mediaFormat.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); } trackOutput.format(mediaFormat); } return readSample(input); }
/** * Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. */ private boolean maybeResynchronize(ExtractorInput extractorInput) throws IOException, InterruptedException { extractorInput.resetPeekPosition(); if (!extractorInput.peekFully(scratch.data, 0, 4, true)) { return false; } scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) { int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData); if (frameSize != -1) { MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); return true; } } synchronizedHeaderData = 0; extractorInput.skipFully(1); return synchronizeCatchingEndOfInput(extractorInput); }
public MpegAudioReader(TrackOutput output) { super(output); state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); headerScratch.data[0] = (byte) 0xFF; header = new MpegAudioHeader(); }
/** * Attempts to read the remaining two bytes of the frame header. * <p> * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. * <p> * If a frame header is read in full but cannot be parsed then the state is changed to * {@link #STATE_READING_HEADER}. * <p> * If a frame header is not read in full then the position of the source is advanced to the limit, * and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); frameBytesRead += bytesToRead; if (frameBytesRead < HEADER_SIZE) { // We haven't read the whole header yet. return; } headerScratch.setPosition(0); boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header); if (!parsedHeader) { // We thought we'd located a frame header, but we hadn't. frameBytesRead = 0; state = STATE_READING_HEADER; return; } frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; MediaFormat mediaFormat = MediaFormat.createAudioFormat(header.mimeType, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, C.UNKNOWN_TIME_US, header.channels, header.sampleRate, null); output.format(mediaFormat); hasOutputFormat = true; } headerScratch.setPosition(0); output.sampleData(headerScratch, HEADER_SIZE); state = STATE_READING_FRAME; }
/** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'XING' or 'INFO' tag. * @param position The position (byte offset) of the start of this frame in the stream. * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, long position, long inputLength) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); // Frame count, size and table of contents are required to use this header. if ((flags & 0x07) != 0x07) { return null; } // Read frame count, as (flags & 1) == 1. int frameCount = frame.readUnsignedIntToInt(); if (frameCount == 0) { return null; } long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate); // Read size in bytes, as (flags & 2) == 2. long sizeBytes = frame.readUnsignedIntToInt(); // Read table-of-contents as (flags & 4) == 4. frame.skipBytes(1); long[] tableOfContents = new long[99]; for (int i = 0; i < 99; i++) { tableOfContents[i] = frame.readUnsignedByte(); } // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); return new XingSeeker(tableOfContents, firstFramePosition, sizeBytes, durationUs, inputLength); }
/** * Attempts to read the remaining two bytes of the frame header. * <p> * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. * <p> * If a frame header is read in full but cannot be parsed then the state is changed to * {@link #STATE_READING_HEADER}. * <p> * If a frame header is not read in full then the position of the source is advanced to the limit, * and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); frameBytesRead += bytesToRead; if (frameBytesRead < HEADER_SIZE) { // We haven't read the whole header yet. return; } headerScratch.setPosition(0); boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header); if (!parsedHeader) { // We thought we'd located a frame header, but we hadn't. frameBytesRead = 0; state = STATE_READING_HEADER; return; } frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, header.mimeType, MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, C.UNKNOWN_TIME_US, header.channels, header.sampleRate, null, null); output.format(mediaFormat); hasOutputFormat = true; } headerScratch.setPosition(0); output.sampleData(headerScratch, HEADER_SIZE); state = STATE_READING_FRAME; }
/** * Constructs a new {@link Mp3Extractor}. * * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or -1 if forcing * is not required. */ public Mp3Extractor(long forcedFirstSampleTimestampUs) { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); basisTimeUs = -1; }
/** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'Xing' or 'Info' tag. * @param position The position (byte offset) of the start of this frame in the stream. * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, long position, long inputLength) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) { // If the frame count is missing/invalid, the header can't be used to determine the duration. return null; } long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND, sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. return new XingSeeker(firstFramePosition, durationUs, inputLength); } long sizeBytes = frame.readUnsignedIntToInt(); frame.skipBytes(1); long[] tableOfContents = new long[99]; for (int i = 0; i < 99; i++) { tableOfContents[i] = frame.readUnsignedByte(); } // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, sizeBytes, mpegAudioHeader.frameSize); }
@Override public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, C.UNKNOWN_TIME_US); seekerWithInputLength = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); xingFrameSize = xingFrameHeader.frameSize; }
/** Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { inputBuffer = new BufferingInput(MpegAudioHeader.MAX_FRAME_SIZE_BYTES * 3); scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); }
@Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray scratch = new ParsableByteArray(4); int startPosition = 0; input.peekFully(scratch.data, 0, 3); if (scratch.readUnsignedInt24() == ID3_TAG) { input.advancePeekPosition(3); input.peekFully(scratch.data, 0, 4); int headerLength = ((scratch.data[0] & 0x7F) << 21) | ((scratch.data[1] & 0x7F) << 14) | ((scratch.data[2] & 0x7F) << 7) | (scratch.data[3] & 0x7F); input.advancePeekPosition(headerLength); startPosition = 3 + 3 + 4 + headerLength; } else { input.resetPeekPosition(); } // Try to find four consecutive valid MPEG audio frames. int headerPosition = startPosition; int validFrameCount = 0; int candidateSynchronizedHeaderData = 0; while (true) { if (headerPosition - startPosition >= MAX_SNIFF_BYTES) { return false; } input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { validFrameCount = 0; candidateSynchronizedHeaderData = 0; // Try reading a header starting at the next byte. input.resetPeekPosition(); input.advancePeekPosition(++headerPosition); continue; } if (validFrameCount == 0) { candidateSynchronizedHeaderData = headerData; } // The header was valid and matching (if appropriate). Check another or end synchronization. if (++validFrameCount == 4) { return true; } // Look for more headers. input.advancePeekPosition(frameSize - 4); } }
/** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. * @param position The position (byte offset) of the start of this frame in the stream. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, long position) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { return null; } int sampleRate = mpegAudioHeader.sampleRate; long durationUs = Util.scaleLargeTimestamp( numFrames, 1000000L * (sampleRate >= 32000 ? 1152 : 576), sampleRate); int numEntries = frame.readUnsignedShort(); int scale = frame.readUnsignedShort(); int entrySize = frame.readUnsignedShort(); // Read entries in the VBRI header. long[] timesUs = new long[numEntries]; long[] offsets = new long[numEntries]; long segmentDurationUs = durationUs / numEntries; long now = 0; int segmentIndex = 0; while (segmentIndex < numEntries) { int numBytes; switch (entrySize) { case 1: numBytes = frame.readUnsignedByte(); break; case 2: numBytes = frame.readUnsignedShort(); break; case 3: numBytes = frame.readUnsignedInt24(); break; case 4: numBytes = frame.readUnsignedIntToInt(); break; default: return null; } now += segmentDurationUs; timesUs[segmentIndex] = now; position += numBytes * scale; offsets[segmentIndex] = position; segmentIndex++; } return new VbriSeeker(timesUs, offsets, position + mpegAudioHeader.frameSize, durationUs); }
private boolean synchronize(ExtractorInput input, boolean sniffing) throws IOException, InterruptedException { int searched = 0; int validFrameCount = 0; int candidateSynchronizedHeaderData = 0; int peekedId3Bytes = 0; input.resetPeekPosition(); if (input.getPosition() == 0) { gaplessInfo = Id3Util.parseId3(input); peekedId3Bytes = (int) input.getPeekPosition(); if (!sniffing) { input.skipFully(peekedId3Bytes); } } while (true) { if (sniffing && searched == MAX_SNIFF_BYTES) { return false; } if (!sniffing && searched == MAX_SYNC_BYTES) { throw new ParserException("Searched too many bytes."); } if (!input.peekFully(scratch.data, 0, 4, true)) { return false; } scratch.setPosition(0); int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { // The header is invalid or doesn't match the candidate header. Try the next byte offset. validFrameCount = 0; candidateSynchronizedHeaderData = 0; searched++; if (sniffing) { input.resetPeekPosition(); input.advancePeekPosition(peekedId3Bytes + searched); } else { input.skipFully(1); } } else { // The header is valid and matches the candidate header. validFrameCount++; if (validFrameCount == 1) { MpegAudioHeader.populateHeader(headerData, synchronizedHeader); candidateSynchronizedHeaderData = headerData; } else if (validFrameCount == 4) { break; } input.advancePeekPosition(frameSize - 4); } } // Prepare to read the synchronized frame. if (sniffing) { input.skipFully(peekedId3Bytes + searched); } else { input.resetPeekPosition(); } synchronizedHeaderData = candidateSynchronizedHeaderData; return true; }
/** * Sets {@link #seeker} to seek using metadata read from {@code input}, which should provide data * from the start of the first frame in the stream. On returning, the input's position will be set * to the start of the first frame of audio. * * @param input The {@link ExtractorInput} from which to read. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already peeked during synchronization. * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already peeked during synchronization. */ private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException { // Read the first frame which may contain a Xing or VBRI header with seeking metadata. ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); long position = input.getPosition(); long length = input.getLength(); // Check if there is a Xing header. int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 frame.setPosition(xingBase); int headerData = frame.readInt(); if (headerData == XING_HEADER || headerData == INFO_HEADER) { seeker = XingSeeker.create(synchronizedHeader, frame, position, length); if (seeker != null && gaplessInfo == null) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); input.advancePeekPosition(xingBase + 141); input.peekFully(scratch.data, 0, 3); scratch.setPosition(0); gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); } else { // Check if there is a VBRI header. frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. headerData = frame.readInt(); if (headerData == VBRI_HEADER) { seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); input.skipFully(synchronizedHeader.frameSize); } } if (seeker == null) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); } }
/** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. * @param position The position (byte offset) of the start of this frame in the stream. * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, long position, long inputLength) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { return null; } int sampleRate = mpegAudioHeader.sampleRate; long durationUs = Util.scaleLargeTimestamp(numFrames, C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate); int entryCount = frame.readUnsignedShort(); int scale = frame.readUnsignedShort(); int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); // Skip the frame containing the VBRI header. position += mpegAudioHeader.frameSize; // Read table of contents entries. long[] timesUs = new long[entryCount + 1]; long[] positions = new long[entryCount + 1]; timesUs[0] = 0L; positions[0] = position; for (int index = 1; index < timesUs.length; index++) { int segmentSize; switch (entrySize) { case 1: segmentSize = frame.readUnsignedByte(); break; case 2: segmentSize = frame.readUnsignedShort(); break; case 3: segmentSize = frame.readUnsignedInt24(); break; case 4: segmentSize = frame.readUnsignedIntToInt(); break; default: return null; } position += segmentSize * scale; timesUs[index] = index * durationUs / entryCount; positions[index] = inputLength == C.LENGTH_UNBOUNDED ? position : Math.min(inputLength, position); } return new VbriSeeker(timesUs, positions, durationUs); }