use of com.google.android.exoplayer2.ExoPlayer in project ExoPlayer by google.
the class DashMediaSource method getAvailableEndTimeInManifestUs.
private static long getAvailableEndTimeInManifestUs(Period period, long periodDurationUs, long nowUnixTimeUs) {
long periodStartTimeInManifestUs = Util.msToUs(period.startMs);
long availableEndTimeInManifestUs = Long.MAX_VALUE;
boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period);
for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
List<Representation> representations = adaptationSet.representations;
// or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029
if ((haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) || representations.isEmpty()) {
continue;
}
@Nullable DashSegmentIndex index = representations.get(0).getIndex();
if (index == null) {
return periodStartTimeInManifestUs + periodDurationUs;
}
long availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
if (availableSegmentCount == 0) {
return periodStartTimeInManifestUs;
}
long firstAvailableSegmentNum = index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
long lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1;
long adaptationSetAvailableEndTimeInManifestUs = periodStartTimeInManifestUs + index.getTimeUs(lastAvailableSegmentNum) + index.getDurationUs(lastAvailableSegmentNum, periodDurationUs);
availableEndTimeInManifestUs = min(availableEndTimeInManifestUs, adaptationSetAvailableEndTimeInManifestUs);
}
return availableEndTimeInManifestUs;
}
use of com.google.android.exoplayer2.ExoPlayer in project ExoPlayer by google.
the class AtomParsers method parseStbl.
/**
* Parses an stbl atom (defined in ISO/IEC 14496-12).
*
* @param track Track to which this sample table corresponds.
* @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information.
* @return Sample table described by the stbl atom.
* @throws ParserException Thrown if the stbl atom can't be parsed.
*/
private static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) throws ParserException {
SampleSizeBox sampleSizeBox;
@Nullable Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) {
sampleSizeBox = new StszSampleSizeBox(stszAtom, track.format);
} else {
@Nullable Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2);
if (stz2Atom == null) {
throw ParserException.createForMalformedContainer("Track has no sample table size information", /* cause= */
null);
}
sampleSizeBox = new Stz2SampleSizeBox(stz2Atom);
}
int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) {
return new TrackSampleTable(track, /* offsets= */
new long[0], /* sizes= */
new int[0], /* maximumSize= */
0, /* timestampsUs= */
new long[0], /* flags= */
new int[0], /* durationUs= */
0);
}
// Entries are byte offsets of chunks.
boolean chunkOffsetsAreLongs = false;
@Nullable Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
if (chunkOffsetsAtom == null) {
chunkOffsetsAreLongs = true;
chunkOffsetsAtom = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_co64));
}
ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;
// Entries are (chunk number, number of samples per chunk, sample description index).
ParsableByteArray stsc = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_stsc)).data;
// Entries are (number of samples, timestamp delta between those samples).
ParsableByteArray stts = checkNotNull(stblAtom.getLeafAtomOfType(Atom.TYPE_stts)).data;
// Entries are the indices of samples that are synchronization samples.
@Nullable Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
@Nullable ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
// Entries are (number of samples, timestamp offset).
@Nullable Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
@Nullable ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
// Prepare to read chunk information.
ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);
// Prepare to read sample timestamps.
stts.setPosition(Atom.FULL_HEADER_SIZE);
int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
// Prepare to read sample timestamp offsets, if ctts is present.
int remainingSamplesAtTimestampOffset = 0;
int remainingTimestampOffsetChanges = 0;
int timestampOffset = 0;
if (ctts != null) {
ctts.setPosition(Atom.FULL_HEADER_SIZE);
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();
}
int nextSynchronizationSampleIndex = C.INDEX_UNSET;
int remainingSynchronizationSamples = 0;
if (stss != null) {
stss.setPosition(Atom.FULL_HEADER_SIZE);
remainingSynchronizationSamples = stss.readUnsignedIntToInt();
if (remainingSynchronizationSamples > 0) {
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
} else {
// Ignore empty stss boxes, which causes all samples to be treated as sync samples.
stss = null;
}
}
// Fixed sample size raw audio may need to be rechunked.
int fixedSampleSize = sampleSizeBox.getFixedSampleSize();
@Nullable String sampleMimeType = track.format.sampleMimeType;
boolean rechunkFixedSizeSamples = fixedSampleSize != C.LENGTH_UNSET && (MimeTypes.AUDIO_RAW.equals(sampleMimeType) || MimeTypes.AUDIO_MLAW.equals(sampleMimeType) || MimeTypes.AUDIO_ALAW.equals(sampleMimeType)) && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 && remainingSynchronizationSamples == 0;
long[] offsets;
int[] sizes;
int maximumSize = 0;
long[] timestamps;
int[] flags;
long timestampTimeUnits = 0;
long duration;
if (rechunkFixedSizeSamples) {
long[] chunkOffsetsBytes = new long[chunkIterator.length];
int[] chunkSampleCounts = new int[chunkIterator.length];
while (chunkIterator.moveNext()) {
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
}
FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
offsets = rechunkedResults.offsets;
sizes = rechunkedResults.sizes;
maximumSize = rechunkedResults.maximumSize;
timestamps = rechunkedResults.timestamps;
flags = rechunkedResults.flags;
duration = rechunkedResults.duration;
} else {
offsets = new long[sampleCount];
sizes = new int[sampleCount];
timestamps = new long[sampleCount];
flags = new int[sampleCount];
long offset = 0;
int remainingSamplesInChunk = 0;
for (int i = 0; i < sampleCount; i++) {
// Advance to the next chunk if necessary.
boolean chunkDataComplete = true;
while (remainingSamplesInChunk == 0 && (chunkDataComplete = chunkIterator.moveNext())) {
offset = chunkIterator.offset;
remainingSamplesInChunk = chunkIterator.numSamples;
}
if (!chunkDataComplete) {
Log.w(TAG, "Unexpected end of chunk data");
sampleCount = i;
offsets = Arrays.copyOf(offsets, sampleCount);
sizes = Arrays.copyOf(sizes, sampleCount);
timestamps = Arrays.copyOf(timestamps, sampleCount);
flags = Arrays.copyOf(flags, sampleCount);
break;
}
// Add on the timestamp offset if ctts is present.
if (ctts != null) {
while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
// The BMFF spec (ISO/IEC 14496-12) states that sample offsets should be unsigned
// integers in version 0 ctts boxes, however some streams violate the spec and use
// signed integers instead. It's safe to always decode sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit
// is set, which is never true in practice because sample offsets are always small).
timestampOffset = ctts.readInt();
remainingTimestampOffsetChanges--;
}
remainingSamplesAtTimestampOffset--;
}
offsets[i] = offset;
sizes[i] = sampleSizeBox.readNextSampleSize();
if (sizes[i] > maximumSize) {
maximumSize = sizes[i];
}
timestamps[i] = timestampTimeUnits + timestampOffset;
// All samples are synchronization samples if the stss is not present.
flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;
if (i == nextSynchronizationSampleIndex) {
flags[i] = C.BUFFER_FLAG_KEY_FRAME;
remainingSynchronizationSamples--;
if (remainingSynchronizationSamples > 0) {
nextSynchronizationSampleIndex = checkNotNull(stss).readUnsignedIntToInt() - 1;
}
}
// Add on the duration of this sample.
timestampTimeUnits += timestampDeltaInTimeUnits;
remainingSamplesAtTimestampDelta--;
if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
// The BMFF spec (ISO/IEC 14496-12) states that sample deltas should be unsigned integers
// in stts boxes, however some streams violate the spec and use signed integers instead.
// See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample
// deltas as signed integers here, because unsigned integers will still be parsed
// correctly (unless their top bit is set, which is never true in practice because sample
// deltas are always small).
timestampDeltaInTimeUnits = stts.readInt();
remainingTimestampDeltaChanges--;
}
offset += sizes[i];
remainingSamplesInChunk--;
}
duration = timestampTimeUnits + timestampOffset;
// If the stbl's child boxes are not consistent the container is malformed, but the stream may
// still be playable.
boolean isCttsValid = true;
if (ctts != null) {
while (remainingTimestampOffsetChanges > 0) {
if (ctts.readUnsignedIntToInt() != 0) {
isCttsValid = false;
break;
}
// Ignore offset.
ctts.readInt();
remainingTimestampOffsetChanges--;
}
}
if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0 || remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0 || remainingSamplesAtTimestampOffset != 0 || !isCttsValid) {
Log.w(TAG, "Inconsistent stbl box for track " + track.id + ": remainingSynchronizationSamples " + remainingSynchronizationSamples + ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta + ", remainingSamplesInChunk " + remainingSamplesInChunk + ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges + ", remainingSamplesAtTimestampOffset " + remainingSamplesAtTimestampOffset + (!isCttsValid ? ", ctts invalid" : ""));
}
}
long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
if (track.editListDurations == null) {
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO && timestamps.length >= 2) {
long editStartTime = checkNotNull(track.editListMediaTimes)[0];
long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0], track.timescale, track.movieTimescale);
if (canApplyEditWithGaplessInfo(timestamps, duration, editStartTime, editEndTime)) {
long paddingTimeUnits = duration - editEndTime;
long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0], track.format.sampleRate, track.timescale);
long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits, track.format.sampleRate, track.timescale);
if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE && encoderPadding <= Integer.MAX_VALUE) {
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
long editedDurationUs = Util.scaleLargeTimestamp(track.editListDurations[0], C.MICROS_PER_SECOND, track.movieTimescale);
return new TrackSampleTable(track, offsets, sizes, maximumSize, timestamps, flags, editedDurationUs);
}
}
}
if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
// The current version of the spec leaves handling of an edit with zero segment_duration in
// unfragmented files open to interpretation. We handle this as a special case and include all
// samples in the edit.
long editStartTime = checkNotNull(track.editListMediaTimes)[0];
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - editStartTime, C.MICROS_PER_SECOND, track.timescale);
}
durationUs = Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
// Omit any sample at the end point of an edit for audio tracks.
boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
// Count the number of samples after applying edits.
int editedSampleCount = 0;
int nextSampleIndex = 0;
boolean copyMetadata = false;
int[] startIndices = new int[track.editListDurations.length];
int[] endIndices = new int[track.editListDurations.length];
long[] editListMediaTimes = checkNotNull(track.editListMediaTimes);
for (int i = 0; i < track.editListDurations.length; i++) {
long editMediaTime = editListMediaTimes[i];
if (editMediaTime != -1) {
long editDuration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale);
startIndices[i] = Util.binarySearchFloor(timestamps, editMediaTime, /* inclusive= */
true, /* stayInBounds= */
true);
endIndices[i] = Util.binarySearchCeil(timestamps, editMediaTime + editDuration, /* inclusive= */
omitClippedSample, /* stayInBounds= */
false);
while (startIndices[i] < endIndices[i] && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// Applying the edit correctly would require prerolling from the previous sync sample. In
// the current implementation we advance to the next sync sample instead. Only other
// tracks (i.e. audio) will be rendered until the time of the first sync sample.
// See https://github.com/google/ExoPlayer/issues/1659.
startIndices[i]++;
}
editedSampleCount += endIndices[i] - startIndices[i];
copyMetadata |= nextSampleIndex != startIndices[i];
nextSampleIndex = endIndices[i];
}
}
copyMetadata |= editedSampleCount != sampleCount;
// Calculate edited sample timestamps and update the corresponding metadata arrays.
long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;
int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;
int editedMaximumSize = copyMetadata ? 0 : maximumSize;
int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;
long[] editedTimestamps = new long[editedSampleCount];
long pts = 0;
int sampleIndex = 0;
for (int i = 0; i < track.editListDurations.length; i++) {
long editMediaTime = track.editListMediaTimes[i];
int startIndex = startIndices[i];
int endIndex = endIndices[i];
if (copyMetadata) {
int count = endIndex - startIndex;
System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
}
for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs = Util.scaleLargeTimestamp(max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale);
editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
editedMaximumSize = sizes[j];
}
sampleIndex++;
}
pts += track.editListDurations[i];
}
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
return new TrackSampleTable(track, editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, editedFlags, editedDurationUs);
}
use of com.google.android.exoplayer2.ExoPlayer in project ExoPlayer by google.
the class Tx3gDecoderTest method decodeWithStyl_startTooLarge_noSpanAdded.
/**
* The 7-byte sample contains a 4-byte emoji. The start index (6) and end index (7) are valid as
* byte offsets, but not a UTF-16 code-unit offset, so they're both truncated to 5 (the length of
* the resulting the string in Java) and the spans end up empty (so we don't add them).
*
* <p>https://github.com/google/ExoPlayer/pull/8133
*/
@Test
public void decodeWithStyl_startTooLarge_noSpanAdded() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_START_TOO_LARGE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC 🙂");
assertThat(text).hasNoSpans();
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
}
use of com.google.android.exoplayer2.ExoPlayer in project ExoPlayer by google.
the class Tx3gDecoderTest method decodeWithStyl_endTooLarge_clippedToEndOfText.
/**
* The 7-byte sample contains a 4-byte emoji. The end index (6) is valid as a byte offset, but not
* a UTF-16 code-unit offset, so it's truncated to 5 (the length of the resulting the string in
* Java).
*
* <p>https://github.com/google/ExoPlayer/pull/8133
*/
@Test
public void decodeWithStyl_endTooLarge_clippedToEndOfText() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_END_TOO_LARGE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC 🙂");
assertThat(text).hasBoldItalicSpanBetween(0, 5);
assertThat(text).hasUnderlineSpanBetween(0, 5);
assertThat(text).hasForegroundColorSpanBetween(0, 5).withColor(Color.GREEN);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
}
use of com.google.android.exoplayer2.ExoPlayer in project ExoPlayer by google.
the class ExoPlayerTest method seekToCurrentPosition_inEndedState_switchesToBufferingStateAndContinuesPlayback.
@Test
public void seekToCurrentPosition_inEndedState_switchesToBufferingStateAndContinuesPlayback() throws Exception {
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount = */
1));
AtomicInteger mediaItemIndexAfterFinalEndedState = new AtomicInteger();
ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED).addMediaSources(mediaSource).executeRunnable(new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(player.getCurrentPosition());
}
}).waitForPlaybackState(Player.STATE_READY).waitForPlaybackState(Player.STATE_ENDED).executeRunnable(new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndexAfterFinalEndedState.set(player.getCurrentMediaItemIndex());
}
}).build();
new ExoPlayerTestRunner.Builder(context).setMediaSources(mediaSource).setActionSchedule(actionSchedule).build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
assertThat(mediaItemIndexAfterFinalEndedState.get()).isEqualTo(1);
}
Aggregations