/** * Parses a udta atom. * * @param udtaAtom The udta (user data) atom to decode. * @param isQuickTime True for QuickTime media. False otherwise. * @param out {@link GaplessInfoHolder} to populate with gapless playback information. */ public static void parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime, GaplessInfoHolder out) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // decode one. return; } ParsableByteArray udtaData = udtaAtom.data; udtaData.setPosition(Atom.HEADER_SIZE); while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { int atomSize = udtaData.readInt(); int atomType = udtaData.readInt(); if (atomType == Atom.TYPE_meta) { udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); udtaData.setLimit(udtaData.getPosition() + atomSize); parseMetaAtom(udtaData, out); break; } udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); } }
private static void parseMetaAtom(ParsableByteArray data, GaplessInfoHolder out) { data.skipBytes(Atom.FULL_HEADER_SIZE); ParsableByteArray ilst = new ParsableByteArray(); while (data.bytesLeft() >= Atom.HEADER_SIZE) { int payloadSize = data.readInt() - Atom.HEADER_SIZE; int atomType = data.readInt(); if (atomType == Atom.TYPE_ilst) { ilst.reset(data.data, data.getPosition() + payloadSize); ilst.setPosition(data.getPosition()); parseIlst(ilst, out); if (out.hasGaplessInfo()) { return; } } data.skipBytes(payloadSize); } }
/** * 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; }
/** * Peeks ID3 data from the input, including gapless playback information. * * @param input The {@link ExtractorInput} from which data should be peeked. * @throws IOException If an error occurred peeking from the input. * @throws InterruptedException If the thread was interrupted. */ private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException { int peekedId3Bytes = 0; while (true) { input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) { // Not an ID3 tag. break; } scratch.skipBytes(3); // Skip major version, minor version and flags. int framesLength = scratch.readSynchSafeInt(); int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength; if (metadata == null) { byte[] id3Data = new byte[tagLength]; System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); // We need to parse enough ID3 metadata to retrieve any gapless playback information even // if ID3 metadata parsing is disabled. Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null; metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } } else { input.advancePeekPosition(framesLength); } peekedId3Bytes += tagLength; } input.resetPeekPosition(); input.advancePeekPosition(peekedId3Bytes); }
private static void parseIlst(ParsableByteArray ilst, GaplessInfoHolder out) { while (ilst.bytesLeft() > 0) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); if (type == Atom.TYPE_DASHES) { String lastCommentMean = null; String lastCommentName = null; String lastCommentData = null; while (ilst.getPosition() < endPosition) { int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; int key = ilst.readInt(); ilst.skipBytes(4); if (key == Atom.TYPE_mean) { lastCommentMean = ilst.readString(length); } else if (key == Atom.TYPE_name) { lastCommentName = ilst.readString(length); } else if (key == Atom.TYPE_data) { ilst.skipBytes(4); lastCommentData = ilst.readString(length - 4); } else { ilst.skipBytes(length); } } if (lastCommentName != null && lastCommentData != null && "com.apple.iTunes".equals(lastCommentMean)) { out.setFromComment(lastCommentName, lastCommentData); break; } } else { ilst.setPosition(endPosition); } } }
/** * Constructs a new {@link Mp3Extractor}. * * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ public Mp3Extractor(long forcedFirstSampleTimestampUs) { this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(4); synchronizedHeader = new MpegAudioHeader(); gaplessInfoHolder = new GaplessInfoHolder(); basisTimeUs = C.TIME_UNSET; }
/** * Peeks data from the input and parses ID3 metadata. * * @param input The {@link ExtractorInput} from which data should be peeked. * @param out The {@link GaplessInfoHolder} to populate. * @throws IOException If an error occurred peeking from the input. * @throws InterruptedException If the thread was interrupted. */ public static void parseId3(ExtractorInput input, GaplessInfoHolder out) throws IOException, InterruptedException { ParsableByteArray scratch = new ParsableByteArray(10); int peekedId3Bytes = 0; while (true) { input.peekFully(scratch.data, 0, 10); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; } int majorVersion = scratch.readUnsignedByte(); int minorVersion = scratch.readUnsignedByte(); int flags = scratch.readUnsignedByte(); int length = scratch.readSynchSafeInt(); if (!out.hasGaplessInfo() && canParseMetadata(majorVersion, minorVersion, flags, length)) { byte[] frame = new byte[length]; input.peekFully(frame, 0, length); parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags, out); } else { input.advancePeekPosition(length); } peekedId3Bytes += 10 + length; } input.resetPeekPosition(); input.advancePeekPosition(peekedId3Bytes); }
/** * @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; }
/** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { long durationUs = C.TIME_UNSET; List<Mp4Track> tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; Metadata metadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { metadata = AtomParsers.parseUdta(udta, isQuickTime); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } } for (int i = 0; i < moov.containerChildren.size(); i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type != Atom.TYPE_trak) { continue; } Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), C.TIME_UNSET, null, isQuickTime); if (track == null) { continue; } Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); if (trackSampleTable.sampleCount == 0) { continue; } Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); if (track.type == C.TRACK_TYPE_AUDIO) { if (gaplessInfoHolder.hasGaplessInfo()) { format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); } if (metadata != null) { format = format.copyWithMetadata(metadata); } } mp4Track.trackOutput.format(format); durationUs = Math.max(durationUs, track.durationUs); tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; if (firstSampleOffset < earliestSampleOffset) { earliestSampleOffset = firstSampleOffset; } } this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); extractorOutput.endTracks(); extractorOutput.seekMap(this); }
/** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { long durationUs = C.TIME_UNSET; List<Mp4Track> tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { AtomParsers.parseUdta(udta, isQuickTime, gaplessInfoHolder); } for (int i = 0; i < moov.containerChildren.size(); i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type != Atom.TYPE_trak) { continue; } Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), C.TIME_UNSET, null, isQuickTime); if (track == null) { continue; } Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); if (trackSampleTable.sampleCount == 0) { continue; } Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); if (track.type == C.TRACK_TYPE_AUDIO && gaplessInfoHolder.hasGaplessInfo()) { format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); } mp4Track.trackOutput.format(format); durationUs = Math.max(durationUs, track.durationUs); tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; if (firstSampleOffset < earliestSampleOffset) { earliestSampleOffset = firstSampleOffset; } } this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); extractorOutput.endTracks(); extractorOutput.seekMap(this); }
/** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { long durationUs = C.TIME_UNSET; List<Mp4Track> tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; Metadata metadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { metadata = AtomParsers.parseUdta(udta, isQuickTime); if (metadata != null) { gaplessInfoHolder.setFromMetadata(metadata); } } for (int i = 0; i < moov.containerChildren.size(); i++) { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type != Atom.TYPE_trak) { continue; } Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); if (track == null) { continue; } Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); if (trackSampleTable.sampleCount == 0) { continue; } Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); if (track.type == C.TRACK_TYPE_AUDIO) { if (gaplessInfoHolder.hasGaplessInfo()) { format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); } if (metadata != null) { format = format.copyWithMetadata(metadata); } } mp4Track.trackOutput.format(format); durationUs = Math.max(durationUs, track.durationUs); tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; if (firstSampleOffset < earliestSampleOffset) { earliestSampleOffset = firstSampleOffset; } } this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); extractorOutput.endTracks(); extractorOutput.seekMap(this); }