@Override public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); if (isFatal) { return Loader.DONT_RETRY_FATAL; } boolean shouldRetry = true; if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) { blacklistUntilMs = SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; notifyPlaylistBlacklisting(playlistUrl, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); } return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY; }
/** * Reads a vorbis identification header from {@code headerData}. * * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis * spec/Identification header</a> * @param headerData a {@link ParsableByteArray} wrapping the header data. * @return a {@link VorbisUtil.VorbisIdHeader} with meta data. * @throws ParserException thrown if invalid capture pattern is detected. */ public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) throws ParserException { verifyVorbisHeaderCapturePattern(0x01, headerData, false); long version = headerData.readLittleEndianUnsignedInt(); int channels = headerData.readUnsignedByte(); long sampleRate = headerData.readLittleEndianUnsignedInt(); int bitrateMax = headerData.readLittleEndianInt(); int bitrateNominal = headerData.readLittleEndianInt(); int bitrateMin = headerData.readLittleEndianInt(); int blockSize = headerData.readUnsignedByte(); int blockSize0 = (int) Math.pow(2, blockSize & 0x0F); int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; // raw data of vorbis setup header has to be passed to decoder as CSD buffer #1 byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, blockSize0, blockSize1, framingFlag, data); }
@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 void processAtomEnded(long atomEndPosition) throws ParserException { while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { Atom.ContainerAtom containerAtom = containerAtoms.pop(); if (containerAtom.type == Atom.TYPE_moov) { // We've reached the end of the moov atom. Process it and prepare to read samples. processMoovAtom(containerAtom); containerAtoms.clear(); parserState = STATE_READING_SAMPLE; } else if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(containerAtom); } } if (parserState != STATE_READING_SAMPLE) { enterReadingAtomHeaderState(); } }
/** * Parses a saio atom (defined in 14496-12). * * @param saio The saio atom to decode. * @param out The {@link TrackFragment} to populate with data from the saio atom. */ private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException { saio.setPosition(Atom.HEADER_SIZE); int fullAtom = saio.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); if ((flags & 0x01) == 1) { saio.skipBytes(8); } int entryCount = saio.readUnsignedIntToInt(); if (entryCount != 1) { // We only support one trun element currently, so always expect one entry. throw new ParserException("Unexpected saio entry count: " + entryCount); } int version = Atom.parseFullAtomVersion(fullAtom); out.auxiliaryDataPosition += version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); }
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) throws ParserException { senc.setPosition(Atom.HEADER_SIZE + offset); int fullAtom = senc.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { // TODO: Implement this. throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported."); } boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; int sampleCount = senc.readUnsignedIntToInt(); if (sampleCount != out.sampleCount) { throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); } Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); out.initEncryptionData(senc.bytesLeft()); out.fillEncryptionData(senc); }
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { TrackBundle nextTrackBundle = null; long nextDataOffset = Long.MAX_VALUE; int trackBundlesSize = trackBundles.size(); for (int i = 0; i < trackBundlesSize; i++) { TrackFragment trackFragment = trackBundles.valueAt(i).fragment; if (trackFragment.sampleEncryptionDataNeedsFill && trackFragment.auxiliaryDataPosition < nextDataOffset) { nextDataOffset = trackFragment.auxiliaryDataPosition; nextTrackBundle = trackBundles.valueAt(i); } } if (nextTrackBundle == null) { parserState = STATE_READING_SAMPLE_START; return; } int bytesToSkip = (int) (nextDataOffset - input.getPosition()); if (bytesToSkip < 0) { throw new ParserException("Offset to encryption data was negative."); } input.skipFully(bytesToSkip); nextTrackBundle.fragment.fillEncryptionData(input); }
void stringElement(int id, String value) throws ParserException { switch (id) { case ID_DOC_TYPE: // Validate that DocType is supported. if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) { throw new ParserException("DocType " + value + " not supported"); } break; case ID_CODEC_ID: currentTrack.codecId = value; break; case ID_LANGUAGE: currentTrack.language = value; break; default: break; } }
/** * 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 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); } } }
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException, InterruptedException { dataScratch.reset(); if (version == 0) { if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) { return false; } // version 0 timestamps are 45kHz, so we need to convert them into us timestampUs = dataScratch.readUnsignedInt() * 1000 / 45; } else if (version == 1) { if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) { return false; } timestampUs = dataScratch.readLong(); } else { throw new ParserException("Unsupported version number: " + version); } remainingSampleCount = dataScratch.readUnsignedByte(); sampleBytesWritten = 0; return true; }
static NoPlayer.PlayerError errorFor(Exception e) { if (e instanceof HttpDataSource.InvalidResponseCodeException) { return new NoPlayerError(PlayerErrorType.INVALID_RESPONSE_CODE, formatMessage(e)); } if (e instanceof ParserException) { return new NoPlayerError(PlayerErrorType.MALFORMED_CONTENT, formatMessage(e)); } Throwable cause = e.getCause(); if (e.getCause() instanceof MediaCodec.CryptoException) { return new NoPlayerError(PlayerErrorType.FAILED_DRM_DECRYPTION, formatMessage(e)); } if (cause instanceof StreamingModularDrm.DrmRequestException) { return new NoPlayerError(PlayerErrorType.FAILED_DRM_REQUEST, formatMessage(e)); } if (e instanceof IOException || cause instanceof IOException) { return new NoPlayerError(PlayerErrorType.CONNECTIVITY_ERROR, cause == null ? formatMessage(e) : formatMessage(cause)); } return new NoPlayerError(PlayerErrorType.UNKNOWN, formatMessage(e)); }
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { type = parseType(parser); putNormalizedAttribute(KEY_TYPE, type); if (type == C.TRACK_TYPE_TEXT) { subType = parseRequiredString(parser, KEY_SUB_TYPE); } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } name = parser.getAttributeValue(null, KEY_NAME); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE); maxHeight = parseInt(parser, KEY_MAX_HEIGHT, Format.NO_VALUE); displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, Format.NO_VALUE); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, Format.NO_VALUE); language = parser.getAttributeValue(null, KEY_LANGUAGE); putNormalizedAttribute(KEY_LANGUAGE, language); timescale = parseInt(parser, KEY_TIME_SCALE, -1); if (timescale == -1) { timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } startTimes = new ArrayList<>(); }
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException { String groupName = ""; ArrayList<Sample> samples = new ArrayList<>(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); switch (name) { case "name": groupName = reader.nextString(); break; case "samples": reader.beginArray(); while (reader.hasNext()) { samples.add(readEntry(reader, false)); } reader.endArray(); break; case "offline_samples": reader.beginArray(); while (reader.hasNext()){ samples.add(readOfflineEntry(reader)); } reader.endArray(); break; case "_comment": reader.nextString(); // Ignore. break; default: throw new ParserException("Unsupported name: " + name); } } reader.endObject(); SampleGroup group = getGroup(groupName, groups); group.samples.addAll(samples); }
private UUID getDrmUuid(String typeString) throws ParserException { switch (typeString.toLowerCase()) { case "widevine": return C.WIDEVINE_UUID; case "playready": return C.PLAYREADY_UUID; default: try { return UUID.fromString(typeString); } catch (RuntimeException e) { throw new ParserException("Unsupported drm type: " + typeString); } } }
@Override public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { boolean isFatal = error instanceof ParserException; eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, isFatal); return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; }
@Override public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) { HlsPlaylist result = loadable.getResult(); if (result instanceof HlsMediaPlaylist) { processLoadedPlaylist((HlsMediaPlaylist) result); eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); } else { onLoadError(loadable, elapsedRealtimeMs, loadDurationMs, new ParserException("Loaded playlist has unexpected type.")); } }
@Override public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); Queue<String> extraLines = new LinkedList<>(); String line; try { if (!checkPlaylistHeader(reader)) { throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.", uri); } while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { // Do nothing. } else if (line.startsWith(TAG_STREAM_INF)) { extraLines.add(line); return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString()); } else if (line.startsWith(TAG_TARGET_DURATION) || line.startsWith(TAG_MEDIA_SEQUENCE) || line.startsWith(TAG_MEDIA_DURATION) || line.startsWith(TAG_KEY) || line.startsWith(TAG_BYTERANGE) || line.equals(TAG_DISCONTINUITY) || line.equals(TAG_DISCONTINUITY_SEQUENCE) || line.equals(TAG_ENDLIST)) { extraLines.add(line); return parseMediaPlaylist(new LineIterator(extraLines, reader), uri.toString()); } else { extraLines.add(line); } } } finally { Util.closeQuietly(reader); } throw new ParserException("Failed to parse the playlist, could not identify any tags."); }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { Matcher matcher = pattern.matcher(line); if (matcher.find() && matcher.groupCount() == 1) { return matcher.group(1); } throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line); }
/** * 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); }
@Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { if (wavHeader == null) { wavHeader = WavHeaderReader.peek(input); if (wavHeader == null) { // Should only happen if the media wasn't sniffed. throw new ParserException("Unsupported or unrecognized wav header."); } Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(), wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null); trackOutput.format(format); bytesPerFrame = wavHeader.getBytesPerFrame(); } if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); extractorOutput.seekMap(this); } int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); if (bytesAppended != RESULT_END_OF_INPUT) { pendingBytes += bytesAppended; } // Samples must consist of a whole number of frames. int pendingFrames = pendingBytes / bytesPerFrame; if (pendingFrames > 0) { long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes); int size = pendingFrames * bytesPerFrame; pendingBytes -= size; trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null); } return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; }
/** * Reads a vorbis comment header. * * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3"> * Vorbis spec/Comment header</a> * @param headerData a {@link ParsableByteArray} wrapping the header data. * @return a {@link VorbisUtil.CommentHeader} with all the comments. * @throws ParserException thrown if invalid capture pattern is detected. */ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) throws ParserException { verifyVorbisHeaderCapturePattern(0x03, headerData, false); int length = 7; int len = (int) headerData.readLittleEndianUnsignedInt(); length += 4; String vendor = headerData.readString(len); length += vendor.length(); long commentListLen = headerData.readLittleEndianUnsignedInt(); String[] comments = new String[(int) commentListLen]; length += 4; for (int i = 0; i < commentListLen; i++) { len = (int) headerData.readLittleEndianUnsignedInt(); length += 4; comments[i] = headerData.readString(len); length += comments[i].length(); } if ((headerData.readUnsignedByte() & 0x01) == 0) { throw new ParserException("framing bit expected to be set"); } length += 1; return new CommentHeader(vendor, comments, length); }
/** * Verifies whether the next bytes in {@code header} are a vorbis header of the given * {@code headerType}. * * @param headerType the type of the header expected. * @param header the alleged header bytes. * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned. * @return the number of bytes read. * @throws ParserException thrown if header type or capture pattern is not as expected. */ public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header, boolean quiet) throws ParserException { if (header.bytesLeft() < 7) { if (quiet) { return false; } else { throw new ParserException("too short header: " + header.bytesLeft()); } } if (header.readUnsignedByte() != headerType) { if (quiet) { return false; } else { throw new ParserException("expected header type " + Integer.toHexString(headerType)); } } if (!(header.readUnsignedByte() == 'v' && header.readUnsignedByte() == 'o' && header.readUnsignedByte() == 'r' && header.readUnsignedByte() == 'b' && header.readUnsignedByte() == 'i' && header.readUnsignedByte() == 's')) { if (quiet) { return false; } else { throw new ParserException("expected characters 'vorbis'"); } } return true; }
/** * This method reads the modes which are located at the very end of the vorbis setup header. * That's why we need to partially decode or at least read the entire setup header to know * where to start reading the modes. * * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4"> * Vorbis spec/Setup header</a> * @param headerData a {@link ParsableByteArray} containing setup header data. * @param channels the number of channels. * @return an array of {@link Mode}s. * @throws ParserException thrown if bit stream is invalid. */ public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) throws ParserException { verifyVorbisHeaderCapturePattern(0x05, headerData, false); int numberOfBooks = headerData.readUnsignedByte() + 1; VorbisBitArray bitArray = new VorbisBitArray(headerData.data); bitArray.skipBits(headerData.getPosition() * 8); for (int i = 0; i < numberOfBooks; i++) { readBook(bitArray); } int timeCount = bitArray.readBits(6) + 1; for (int i = 0; i < timeCount; i++) { if (bitArray.readBits(16) != 0x00) { throw new ParserException("placeholder of time domain transforms not zeroed out"); } } readFloors(bitArray); readResidues(bitArray); readMappings(channels, bitArray); Mode[] modes = readModes(bitArray); if (!bitArray.readBit()) { throw new ParserException("framing bit after modes not set as expected"); } return modes; }
private static void readResidues(VorbisBitArray bitArray) throws ParserException { int residueCount = bitArray.readBits(6) + 1; for (int i = 0; i < residueCount; i++) { int residueType = bitArray.readBits(16); if (residueType > 2) { throw new ParserException("residueType greater than 2 is not decodable"); } else { bitArray.skipBits(24); // begin bitArray.skipBits(24); // end bitArray.skipBits(24); // partitionSize (add one) int classifications = bitArray.readBits(6) + 1; bitArray.skipBits(8); // classbook int[] cascade = new int[classifications]; for (int j = 0; j < classifications; j++) { int highBits = 0; int lowBits = bitArray.readBits(3); if (bitArray.readBit()) { highBits = bitArray.readBits(5); } cascade[j] = highBits * 8 + lowBits; } for (int j = 0; j < classifications; j++) { for (int k = 0; k < 8; k++) { if ((cascade[j] & (0x01 << k)) != 0) { bitArray.skipBits(8); // discard } } } } } }
public static boolean verifyBitstreamType(ParsableByteArray data) { try { return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); } catch (ParserException e) { return false; } }
/** * Parses a trak atom (defined in 14496-12). * * @param trak Atom to decode. * @param mvhd Movie header atom, used to get the timescale. * @param duration The duration in units of the timescale declared in the mvhd atom, or * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. * @param drmInitData {@link DrmInitData} to be included in the format. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, DrmInitData drmInitData, boolean isQuickTime) throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); if (trackType == C.TRACK_TYPE_UNKNOWN) { return null; } TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); if (duration == C.TIME_UNSET) { duration = tkhdData.duration; } long movieTimescale = parseMvhd(mvhd.data); long durationUs; if (duration == C.TIME_UNSET) { durationUs = C.TIME_UNSET; } else { durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale); } Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) .getContainerAtomOfType(Atom.TYPE_stbl); Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); }
/** * Parses a stsd atom (defined in 14496-12). * * @param stsd The stsd atom to decode. * @param trackId The track's identifier in its container. * @param rotationDegrees The rotation of the track in degrees. * @param language The language of the track. * @param drmInitData {@link DrmInitData} to be included in the format. * @param isQuickTime True for QuickTime media. False otherwise. * @return An object containing the parsed data. */ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees, String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException { stsd.setPosition(Atom.FULL_HEADER_SIZE); int numberOfEntries = stsd.readInt(); StsdData out = new StsdData(numberOfEntries); for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 || childAtomType == Atom.TYPE_s263 || childAtomType == Atom.TYPE_vp08 || childAtomType == Atom.TYPE_vp09) { parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, rotationDegrees, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3 || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, drmInitData, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); } stsd.setPosition(childStartPosition + childAtomSize); } return out; }
private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. List<byte[]> initializationData = null; long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE; String mimeType; if (atomType == Atom.TYPE_TTML) { mimeType = MimeTypes.APPLICATION_TTML; } else if (atomType == Atom.TYPE_tx3g) { mimeType = MimeTypes.APPLICATION_TX3G; int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8; byte[] sampleDescriptionData = new byte[sampleDescriptionLength]; parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength); initializationData = Collections.singletonList(sampleDescriptionData); } else if (atomType == Atom.TYPE_wvtt) { mimeType = MimeTypes.APPLICATION_MP4VTT; } else if (atomType == Atom.TYPE_stpp) { mimeType = MimeTypes.APPLICATION_TTML; subsampleOffsetUs = 0; // Subsample timing is absolute. } else if (atomType == Atom.TYPE_c608) { // Defined by the QuickTime File Format specification. mimeType = MimeTypes.APPLICATION_MP4CEA608; out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; } else { // Never happens. throw new IllegalStateException(); } out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, initializationData); }
private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException { if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(leaf); } else if (leaf.type == Atom.TYPE_sidx) { Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition); segmentIndexEarliestPresentationTimeUs = result.first; extractorOutput.seekMap(result.second); haveOutputSeekMap = true; } else if (leaf.type == Atom.TYPE_emsg) { onEmsgLeafAtomRead(leaf.data); } }
private void onContainerAtomRead(ContainerAtom container) throws ParserException { if (container.type == Atom.TYPE_moov) { onMoovContainerAtomRead(container); } else if (container.type == Atom.TYPE_moof) { onMoofContainerAtomRead(container); } else if (!containerAtoms.isEmpty()) { containerAtoms.peek().add(container); } }
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, extendedTypeScratch); DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { trackBundles.valueAt(i).updateDrmInitData(drmInitData); } } }
private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { int moofContainerChildrenSize = moof.containerChildren.size(); for (int i = 0; i < moofContainerChildrenSize; i++) { Atom.ContainerAtom child = moof.containerChildren.get(i); // TODO: Support multiple traf boxes per track in a single moof. if (child.type == Atom.TYPE_traf) { parseTraf(child, trackBundleArray, flags, extendedTypeScratch); } } }