diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt index c59a42fb..478dd3f5 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt +++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/codec/Decoder.kt @@ -42,6 +42,7 @@ class Decoder( companion object { private val ID = trackMapOf(AtomicInteger(0), AtomicInteger(0)) private const val timeoutUs = 2000L + private const val VERBOSE = false } private val log = Logger("Decoder(${format.trackType},${ID[format.trackType].getAndIncrement()})") @@ -90,6 +91,9 @@ class Decoder( dequeuedInputs-- val (chunk, id) = data val flag = if (chunk.keyframe) BUFFER_FLAG_SYNC_FRAME else 0 + if(VERBOSE) { + log.v("enqueue(): queueInputBuffer ${chunk.timeUs}") + } codec.queueInputBuffer(id, chunk.buffer.position(), chunk.buffer.remaining(), chunk.timeUs, flag) dropper.input(chunk.timeUs, chunk.render) } @@ -123,11 +127,17 @@ class Decoder( // Ideally, we shouldn't rely on the fact that the buffer is properly configured. // We should configure its position and limit based on the buffer info's position and size. val data = DecoderData(buffer, timeUs) { + if(VERBOSE) { + log.v("drain(): released successfully presentation ts ${info.presentationTimeUs} and $timeUs") + } codec.releaseOutputBuffer(result, it) dequeuedOutputs-- } if (isEos) State.Eos(data) else State.Ok(data) } else { + if(VERBOSE) { + log.v("drain(): released because decoder dropper gave null ts ${info.presentationTimeUs}") + } codec.releaseOutputBuffer(result, false) State.Wait }.also { diff --git a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt index e2b63eb4..9dda8495 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt +++ b/lib/src/main/java/com/otaliastudios/transcoder/internal/thumbnails/DefaultThumbnailsEngine.kt @@ -88,6 +88,8 @@ class DefaultThumbnailsEngine( return source.keyFrameTimestamps } + override fun getSeekThreshold() = source.seekThreshold + override fun isDrained(): Boolean { if (source.isDrained) { source.seekTo(stubs.firstOrNull()?.positionUs ?: -1) @@ -121,15 +123,20 @@ class DefaultThumbnailsEngine( val nextKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex) { Long.MAX_VALUE } val previousKeyFrameUs = source.keyFrameAt(nextKeyFrameIndex - 1) { source.lastKeyFrame() } - log.i( - "seek: current ${source.positionUs}," + - " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs" - ) val rightGap = nextKeyFrameUs - requested val nextKeyFrameInThreshold = rightGap <= threshold seek = nextKeyFrameInThreshold || previousKeyFrameUs > current || (current - requested > threshold) - seekUs = if (nextKeyFrameInThreshold) nextKeyFrameUs else previousKeyFrameUs + seekUs = + (if (nextKeyFrameInThreshold) nextKeyFrameUs else previousKeyFrameUs) + source.seekThreshold + + if (VERBOSE) { + log.i( + "seek: current ${source.positionUs}," + + " requested $requested, threshold $threshold, nextKeyFrameUs $nextKeyFrameUs," + + " nextKeyFrameInThreshold:$nextKeyFrameInThreshold, seekUs: $seekUs, flushing : $seek" + ) + } shouldFlush = seek shouldSeek = false @@ -287,5 +294,6 @@ class DefaultThumbnailsEngine( companion object { private const val WAIT_MS = 5L + private const val VERBOSE = false } } diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java index ab469fec..030756fa 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java +++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java @@ -118,6 +118,10 @@ default ArrayList getKeyFrameTimestamps() { return new ArrayList<>(); } + default long getSeekThreshold() { + return 0; + } + default String mediaId() { return "";} /** * Rewinds this source, moving it to its default state. * To be used again, tracks will be selected again. diff --git a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java index 7e7c1beb..7eddb98d 100644 --- a/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java +++ b/lib/src/main/java/com/otaliastudios/transcoder/source/DefaultDataSource.java @@ -46,6 +46,8 @@ public abstract class DefaultDataSource implements DataSource { private long mDontRenderRangeEnd = -1L; private final ArrayList keyFrameTimestamps = new ArrayList<>(); + private final long SEEK_THRESHOLD = 10001L; // 10ms because extractor doesn't seek accurately + private final boolean VERBOSE = false; @Override public void initialize() { LOG.i("initialize(): initializing..."); @@ -102,14 +104,14 @@ public long requestKeyFrameTimestamps() { if(lastKeyFrame) return -1L; if(keyFrameTimestamps.size() > 0) { - mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC); + mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_NEXT_SYNC); } long sampleTime = mExtractor.getSampleTime(); if (sampleTime == -1 || (keyFrameTimestamps.size() > 0 && sampleTime == keyFrameTimestamps.get(keyFrameTimestamps.size() - 1))) { lastKeyFrame = true; - mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + 10001L, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + mExtractor.seekTo(keyFrameTimestamps.get(keyFrameTimestamps.size() - 1) + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); return -1; } LOG.i("keyFrameStartTime:" + sampleTime); @@ -124,7 +126,7 @@ public long requestKeyFrameTimestamps() { } keyFrameTimestamps.add(sampleTime); } - mExtractor.seekTo(sampleTime + 10001L, MediaExtractor.SEEK_TO_NEXT_SYNC); + mExtractor.seekTo(sampleTime + SEEK_THRESHOLD, MediaExtractor.SEEK_TO_NEXT_SYNC); lastSampleTime = sampleTime; sampleTime = mExtractor.getSampleTime(); count++; @@ -134,6 +136,11 @@ public long requestKeyFrameTimestamps() { } + @Override + public long getSeekThreshold() { + return SEEK_THRESHOLD; + } + @Override public void deinitialize() { LOG.i("deinitialize(): deinitializing..."); @@ -189,13 +196,15 @@ public void releaseTrack(@NonNull TrackType type) { public long seekTo(long desiredPositionUs) { boolean hasVideo = mSelectedTracks.contains(TrackType.VIDEO); boolean hasAudio = mSelectedTracks.contains(TrackType.AUDIO); - LOG.i("seekTo(): seeking to " + (mOriginUs + desiredPositionUs) - + " originUs=" + mOriginUs - + " extractorUs=" + mExtractor.getSampleTime() - + " externalUs=" + desiredPositionUs - + " hasVideo=" + hasVideo - + " hasAudio=" + hasAudio); if (hasVideo && hasAudio) { + if (VERBOSE) { + LOG.i("seekTo(): seeking to " + (mOriginUs + desiredPositionUs) + + " originUs=" + mOriginUs + + " extractorUs=" + mExtractor.getSampleTime() + + " externalUs=" + desiredPositionUs + + " hasVideo=" + hasVideo + + " hasAudio=" + hasAudio); + } // Special case: audio can be moved to any timestamp, but video will only stop in // sync frames. MediaExtractor is not smart enough to sync the two tracks at the // video sync frame, so we must take care of this with the following trick. @@ -208,6 +217,14 @@ public long seekTo(long desiredPositionUs) { mExtractor.seekTo(mExtractor.getSampleTime(), MediaExtractor.SEEK_TO_CLOSEST_SYNC); LOG.v("seekTo(): seek workaround completed. (extractorUs=" + mExtractor.getSampleTime() + ")"); } else { + if (VERBOSE) { + LOG.i("seekTo(): seeking to " + (desiredPositionUs) + + " originUs=" + mOriginUs + + " extractorUs=" + mExtractor.getSampleTime() + + " externalUs=" + desiredPositionUs + + " hasVideo=" + hasVideo + + " hasAudio=" + hasAudio); + } mExtractor.seekTo(desiredPositionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); } mDontRenderRangeStart = mExtractor.getSampleTime();