/** * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. */ public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.mode = mode; if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { timestampAdjusters = Collections.singletonList(timestampAdjuster); } else { timestampAdjusters = new ArrayList<>(); timestampAdjusters.add(timestampAdjuster); } tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); resetPayloadReaders(); }
private static ApicFrame parseCoverArt(ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { int fullVersionInt = data.readInt(); int flags = Atom.parseFullAtomFlags(fullVersionInt); String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null; if (mimeType == null) { Log.w(TAG, "Unrecognized cover art flags: " + flags); return null; } data.skipBytes(4); // empty (4) byte[] pictureData = new byte[atomSize - 16]; data.readBytes(pictureData, 0, pictureData.length); return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData); } Log.w(TAG, "Failed to parse cover art attribute"); return null; }
/** * Locates the next syncword, advancing the position to the byte that immediately follows it. If a * syncword was not located, the position is advanced to the limit. * * @param pesBuffer The buffer whose position should be advanced. * @return Whether a syncword position was found. */ private boolean skipToNextSync(ParsableByteArray pesBuffer) { while (pesBuffer.bytesLeft() > 0) { if (!lastByteWas0B) { lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B; continue; } int secondByte = pesBuffer.readUnsignedByte(); if (secondByte == 0x77) { lastByteWas0B = false; return true; } else { lastByteWas0B = secondByte == 0x0B; } } return false; }
@Override public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: findHeader(data); break; case STATE_READING_HEADER: readHeaderRemainder(data); break; case STATE_READING_FRAME: readFrameRemainder(data); break; } } }
private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: return readAmfDouble(data); case AMF_TYPE_BOOLEAN: return readAmfBoolean(data); case AMF_TYPE_STRING: return readAmfString(data); case AMF_TYPE_OBJECT: return readAmfObject(data); case AMF_TYPE_ECMA_ARRAY: return readAmfEcmaArray(data); case AMF_TYPE_STRICT_ARRAY: return readAmfStrictArray(data); case AMF_TYPE_DATE: return readAmfDate(data); default: return null; } }
@Override protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { // Should never happen. throw new ParserException(); } String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { // We're only interested in metadata. return; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { // We're not interested in this metadata. return; } // Set the duration to the value contained in the metadata, if present. Map<String, Object> metadata = readAmfEcmaArray(data); if (metadata.containsKey(KEY_DURATION)) { double durationSeconds = (double) metadata.get(KEY_DURATION); if (durationSeconds > 0.0) { durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } }
/** * Parses an MS/ACM codec private, returning whether it indicates PCM audio. * * @return Whether the codec private indicates PCM audio. * @throws ParserException If a parsing error occurs. */ private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException { try { int formatTag = buffer.readLittleEndianUnsignedShort(); if (formatTag == WAVE_FORMAT_PCM) { return true; } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) { buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4) return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits() && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits(); } else { return false; } } catch (ArrayIndexOutOfBoundsException e) { throw new ParserException("Error parsing MS/ACM codec private"); } }
@Override public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: findNextSample(data); break; case STATE_READING_ID3_HEADER: if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) { parseId3Header(); } break; case STATE_READING_ADTS_HEADER: int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.data, targetLength)) { parseAdtsHeader(); } break; case STATE_READING_SAMPLE: readSample(data); break; } } }
@Override public void consume(ParsableByteArray sectionData) { if (!formatDeclared) { if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { // There is not enough information to initialize the timestamp adjuster. return; } output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, timestampAdjuster.getTimestampOffsetUs())); formatDeclared = true; } int sampleSize = sectionData.bytesLeft(); output.sampleData(sectionData, sampleSize); output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); }
private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { byte[] data = new byte[frameSize]; id3Data.readBytes(data, 0, frameSize); int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; if (privateDataStartIndex < data.length) { privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); } else { privateData = new byte[0]; } return new PrivFrame(owner, privateData); }
@Override public void consume(ParsableByteArray data) { if (writingSample) { if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) { // Failed to check data_identifier return; } if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) { // Check and discard the subtitle_stream_id return; } int dataPosition = data.getPosition(); int bytesAvailable = data.bytesLeft(); for (TrackOutput output : outputs) { data.setPosition(dataPosition); output.sampleData(data, bytesAvailable); } sampleBytesWritten += bytesAvailable; } }
@Override protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { if (vorbisSetup != null) { return false; } vorbisSetup = readSetupHeaders(packet); if (vorbisSetup == null) { return true; } ArrayList<byte[]> codecInitialisationData = new ArrayList<>(); codecInitialisationData.add(vorbisSetup.idHeader.data); codecInitialisationData.add(vorbisSetup.setupHeaderData); setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, codecInitialisationData, null, 0, null); return true; }
@Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { OggPageHeader header = new OggPageHeader(); if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { return false; } int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); ParsableByteArray scratch = new ParsableByteArray(length); input.peekFully(scratch.data, 0, length); if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { streamReader = new FlacReader(); } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { streamReader = new VorbisReader(); } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { streamReader = new OpusReader(); } else { return false; } return true; } catch (ParserException e) { return false; } }
private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder, int remainingCueBoxBytes) throws SubtitleDecoderException { builder.reset(); while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { throw new SubtitleDecoderException("Incomplete vtt cue box header found."); } int boxSize = sampleData.readInt(); int boxType = sampleData.readInt(); remainingCueBoxBytes -= BOX_HEADER_SIZE; int payloadLength = boxSize - BOX_HEADER_SIZE; String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { WebvttCueParser.parseCueSettingsList(boxPayload, builder); } else if (boxType == TYPE_payl) { WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.<WebvttCssStyle>emptyList()); } else { // Other VTTCueBox children are still not supported and are ignored. } } return builder.build(); }
@Override protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { if (!headerRead) { byte[] metadata = Arrays.copyOf(packet.data, packet.limit()); int channelCount = metadata[9] & 0xFF; int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF); List<byte[]> initializationData = new ArrayList<>(3); initializationData.add(metadata); putNativeOrderLong(initializationData, preskip); putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES); setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0, null); headerRead = true; } else { boolean headerPacket = packet.readInt() == OPUS_CODE; packet.setPosition(0); return headerPacket; } return true; }
/** * Reads lines up to and including the next WebVTT cue header. * * @param input The input from which lines should be read. * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was * reached without a cue header being found. In the case that a cue header is found, groups 1, * 2 and 3 of the returned matcher contain the start time, end time and settings list. */ public static Matcher findNextCueHeader(ParsableByteArray input) { String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. while ((line = input.readLine()) != null && !line.isEmpty()) {} } else { Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line); if (cueHeaderMatcher.matches()) { return cueHeaderMatcher; } } } return null; }
private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) { stringBuilder.setLength(0); int position = input.getPosition(); int limit = input.limit(); boolean identifierEndFound = false; while (position < limit && !identifierEndFound) { char c = (char) input.data[position]; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#' || c == '-' || c == '.' || c == '_') { position++; stringBuilder.append(c); } else { identifierEndFound = true; } } input.skipBytes(position - input.getPosition()); return stringBuilder.toString(); }
/** * Parses encryption data from an audio/video sample entry, populating {@code out} and returning * the unencrypted atom type, or 0 if no common encryption sinf atom was present. */ private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, int size, StsdData out, int entryIndex) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition, childAtomSize); if (result != null) { out.trackEncryptionBoxes[entryIndex] = result.second; return result.first; } } childPosition += childAtomSize; } // This enca/encv box does not have a data format so return an invalid atom type. return 0; }
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_tenc) { parent.skipBytes(6); boolean defaultIsEncrypted = parent.readUnsignedByte() == 1; int defaultInitVectorSize = parent.readUnsignedByte(); byte[] defaultKeyId = new byte[16]; parent.readBytes(defaultKeyId, 0, defaultKeyId.length); return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); } childPosition += childAtomSize; } return null; }
private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data && atomSize >= 22) { data.skipBytes(10); // version (1), flags (3), empty (4), empty (2) int index = data.readUnsignedShort(); if (index > 0) { String value = "" + index; int count = data.readUnsignedShort(); if (count > 0) { value += "/" + count; } return new TextInformationFrame(attributeName, null, value); } } Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type)); return null; }
/** * Parses an mehd atom (defined in 14496-12). */ private static long parseMehd(ParsableByteArray mehd) { mehd.setPosition(Atom.HEADER_SIZE); int fullAtom = mehd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong(); }
private static int parseUint8AttributeValue(ParsableByteArray data) { data.skipBytes(4); // atomSize int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { data.skipBytes(8); // version (1), flags (3), empty (4) return data.readUnsignedByte(); } Log.w(TAG, "Failed to parse uint8 attribute value"); return -1; }
private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize, int majorVersion) throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); byte[] data = new byte[frameSize - 1]; id3Data.readBytes(data, 0, frameSize - 1); String mimeType; int mimeTypeEndIndex; if (majorVersion == 2) { mimeTypeEndIndex = 2; mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); if (mimeType.equals("image/jpg")) { mimeType = "image/jpeg"; } } else { mimeTypeEndIndex = indexOfZeroByte(data, 0); mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1")); if (mimeType.indexOf('/') == -1) { mimeType = "image/" + mimeType; } } int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; int descriptionStartIndex = mimeTypeEndIndex + 2; int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); String description = new String(data, descriptionStartIndex, descriptionEndIndex - descriptionStartIndex, charset); int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); return new ApicFrame(mimeType, description, pictureType, pictureData); }
static SpliceScheduleCommand parseFromSection(ParsableByteArray sectionData) { int spliceCount = sectionData.readUnsignedByte(); ArrayList<Event> events = new ArrayList<>(spliceCount); for (int i = 0; i < spliceCount; i++) { events.add(Event.parseFromSection(sectionData)); } return new SpliceScheduleCommand(events); }
/** * Constructs a new reader for DTS elementary streams. * * @param language Track language. */ public DtsReader(String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF); headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF); state = STATE_FINDING_SYNC; this.language = language; }
/** * Returns a string containing the selector. The input is expected to have the form * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. * @return A string containing the target, empty string if the selector is universal * (targets all cues) or null if an error was encountered. */ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { return null; } String cueSelector = input.readString(5); if (!"::cue".equals(cueSelector)) { return null; } int position = input.getPosition(); String token = parseNextToken(input, stringBuilder); if (token == null) { return null; } if (BLOCK_START.equals(token)) { input.setPosition(position); return ""; } String target = null; if ("(".equals(token)) { target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); if (!")".equals(token) || token == null) { return null; } return target; }
/** * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately * follows it. If SYNC was not located, the position is advanced to the limit. * * @param pesBuffer The buffer whose position should be advanced. * @return Whether SYNC was found. */ private boolean skipToNextSync(ParsableByteArray pesBuffer) { while (pesBuffer.bytesLeft() > 0) { syncBytes <<= 8; syncBytes |= pesBuffer.readUnsignedByte(); if (syncBytes == SYNC_VALUE) { syncBytes = 0; return true; } } return false; }
/** * Continues a read from the provided {@code source} into a given {@code target}. It's assumed * that the data should be written into {@code target} starting from an offset of zero. * * @param source The source from which to read. * @param target The target into which data is to be read, or {@code null} to skip. * @param targetLength The target length of the read. * @return Whether the target length has been reached. */ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); if (bytesToRead <= 0) { return true; } else if (target == null) { source.skipBytes(bytesToRead); } else { source.readBytes(target, bytesRead, bytesToRead); } bytesRead += bytesToRead; return bytesRead == targetLength; }
/** * Constructs a new reader for (E-)AC-3 elementary streams. * * @param language Track language. */ public Ac3Reader(String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; this.language = language; }
@Override public void consume(ParsableByteArray data) { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: if (skipToNextSync(data)) { state = STATE_READING_HEADER; headerScratchBytes.data[0] = 0x0B; headerScratchBytes.data[1] = 0x77; bytesRead = 2; } break; case STATE_READING_HEADER: if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { parseHeader(); headerScratchBytes.setPosition(0); output.sampleData(headerScratchBytes, HEADER_SIZE); state = STATE_READING_SAMPLE; } break; case STATE_READING_SAMPLE: int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); output.sampleData(data, bytesToRead); bytesRead += bytesToRead; if (bytesRead == sampleSize) { output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); timeUs += sampleDurationUs; state = STATE_FINDING_SYNC; } break; default: break; } } }
public MpegAudioReader(String language) { 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(); this.language = language; }
static PrivateCommand parseFromSection(ParsableByteArray sectionData, int commandLength, long ptsAdjustment) { long identifier = sectionData.readUnsignedInt(); byte[] privateBytes = new byte[commandLength - 4 /* identifier size */]; sectionData.readBytes(privateBytes, 0, privateBytes.length); return new PrivateCommand(identifier, privateBytes, ptsAdjustment); }
/** * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer * to any {@link TrackBundle}, {@code null} is returned and no changes are made. * * @param tfhd The tfhd atom to decode. * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed. * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * does not refer to any {@link TrackBundle}. */ private static TrackBundle parseTfhd(ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles, int flags) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); int trackId = tfhd.readInt(); TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); if (trackBundle == null) { return null; } if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { long baseDataPosition = tfhd.readUnsignedLongToLong(); trackBundle.fragment.dataPosition = baseDataPosition; trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; } DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, defaultSampleSize, defaultSampleFlags); return trackBundle; }
/** * Reads the rest of the sample */ private void readSample(ParsableByteArray data) { int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); currentOutput.sampleData(data, bytesToRead); bytesRead += bytesToRead; if (bytesRead == sampleSize) { currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); timeUs += currentSampleDuration; setFindingSampleState(); } }
/** * Process an ftyp atom to determine whether the media is QuickTime. * * @param atomData The ftyp atom data. * @return Whether the media is QuickTime. */ private static boolean processFtypAtom(ParsableByteArray atomData) { atomData.setPosition(Atom.HEADER_SIZE); int majorBrand = atomData.readInt(); if (majorBrand == BRAND_QUICKTIME) { return true; } atomData.skipBytes(4); // minor_version while (atomData.bytesLeft() > 0) { if (atomData.readInt() == BRAND_QUICKTIME) { return true; } } return false; }
private static boolean maybeSkipWhitespace(ParsableByteArray input) { switch(peekCharAtPosition(input, input.getPosition())) { case '\t': case '\r': case '\n': case '\f': case ' ': input.skipBytes(1); return true; default: return false; } }
private boolean checkNextByte(ParsableByteArray data, int expectedValue) { if (data.bytesLeft() == 0) { return false; } if (data.readUnsignedByte() != expectedValue) { writingSample = false; } bytesToCheck--; return writingSample; }
/** * Skips to the data in the given WAV input stream and returns its data size. After calling, the * input stream's position will point to the start of sample data in the WAV. * <p> * If an exception is thrown, the input position will be left pointing to a chunk header. * * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to * a valid chunk header. * @param wavHeader WAV header to populate with data bounds. * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from input. */ public static void skipToData(ExtractorInput input, WavHeader wavHeader) throws IOException, InterruptedException { Assertions.checkNotNull(input); Assertions.checkNotNull(wavHeader); // Make sure the peek position is set to the read position before we peek the first header. input.resetPeekPosition(); ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); while (chunkHeader.id != Util.getIntegerCodeForString("data")) { Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); } input.skipFully((int) bytesToSkip); chunkHeader = ChunkHeader.peek(input, scratch); } // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); }
/** * Peeks and returns a {@link ChunkHeader}. * * @param input Input stream to peek the chunk header from. * @param scratch Buffer for temporary use. * @throws IOException If peeking from the input fails. * @throws InterruptedException If interrupted while peeking from input. * @return A new {@code ChunkHeader} peeked from {@code input}. */ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) throws IOException, InterruptedException { input.peekFully(scratch.data, 0, SIZE_IN_BYTES); scratch.setPosition(0); int id = scratch.readInt(); long size = scratch.readLittleEndianUnsignedInt(); return new ChunkHeader(id, size); }
/** * Constructs a new {@link Mp3Extractor}. * * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; }