Skip to content

Commit 1a54498

Browse files
authored
fix: keep a seek near the end of the source and the position after seeking back (#328)
A seek made while the decode thread waited for the queue to play out, which is the last second or two of any source and all of a short one, was taken for the end of the source: the seek emptied the queue, the wait saw it drained, and the player reported the end of the stream and stopped, with the seek still pending. The wait now ends when a seek is pending, and the player carries the seek out instead of ending. Audio only ever moves the position forward, so a position left over from before a seek backwards held an audio-only source at where it had been until the audio caught up. Flushing the pacer now sets the position, and a seek passes its target. The tests fail without the fixes: a seek to the start at 0.7 s of a two second clip played no more frames, and 0.4 s after seeking an audio file back from 1.5 s the position still read 1.5 s.
1 parent 58253b0 commit 1a54498

4 files changed

Lines changed: 77 additions & 7 deletions

File tree

‎webrtc-java-media/src/main/cpp/include/media/MediaPacer.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ namespace ffmpeg
104104

105105
// Drops everything queued and forgets the clock mapping, so that
106106
// the next item delivered starts a new one. This is what a seek
107-
// needs: what is queued belongs to the position being left.
108-
void Flush();
107+
// needs: what is queued belongs to the position being left. The
108+
// position is where playback is taken to be until an item has been
109+
// delivered, in the timestamps the items are pushed with.
110+
void Flush(int64_t position_us);
109111

110112
// True once everything handed over has been delivered.
111113
bool IsDrained() const;

‎webrtc-java-media/src/main/cpp/src/media/MediaPacer.cpp‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,17 @@ namespace ffmpeg
219219
work_.notify_all();
220220
}
221221

222-
void MediaPacer::Flush()
222+
void MediaPacer::Flush(int64_t position_us)
223223
{
224224
std::lock_guard<std::mutex> lock(mutex_);
225225

226226
video_queue_.clear();
227227
audio_queue_.clear();
228228

229+
// Audio only ever moves the position forward, so a position left from
230+
// before a seek backwards would hold it there until audio caught up.
231+
position_us_ = position_us;
232+
229233
// The next item delivered starts a new mapping, wherever it comes from.
230234
have_base_ = false;
231235

‎webrtc-java-media/src/main/cpp/src/media/MediaPlayer.cpp‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ namespace ffmpeg
174174
// on the request until it gets out of that. Dropping what is queued
175175
// both frees it and throws away what belongs to the old position; the
176176
// thread flushes again once the seek has actually happened.
177-
pacer_->Flush();
177+
pacer_->Flush(position_us + loop_offset_us_.load());
178178
}
179179

180180
void MediaPlayer::SetLooping(bool looping)
@@ -317,11 +317,13 @@ namespace ffmpeg
317317
continue;
318318
}
319319

320-
// Everything has been handed over, but not yet played out.
320+
// Everything has been handed over, but not yet played out. A seek
321+
// ends the wait too: it empties the queue, which would otherwise
322+
// look like the source having played out.
321323
for (;;) {
322324
std::unique_lock<std::mutex> lock(mutex_);
323325

324-
if (closing_ || pacer_->IsDrained()) {
326+
if (closing_ || seek_pending_ || pacer_->IsDrained()) {
325327
break;
326328
}
327329

@@ -335,6 +337,12 @@ namespace ffmpeg
335337
break;
336338
}
337339

340+
if (seek_pending_) {
341+
// Not at the end any more; the seek is carried out at the
342+
// top, and playback goes on from where it lands.
343+
continue;
344+
}
345+
338346
ended_ = true;
339347
playing_ = false;
340348
}
@@ -492,7 +500,7 @@ namespace ffmpeg
492500
loop_offset_us_.store(0);
493501
max_source_pts_us_ = position_us;
494502

495-
pacer_->Flush();
503+
pacer_->Flush(position_us);
496504
}
497505

498506
void MediaPlayer::SetState(int state)

‎webrtc-java-media/src/test/java/dev/onvoid/webrtc/media/player/MediaPlayerTest.java‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,62 @@ void seekMovesPlayback() throws Exception {
279279
}
280280
}
281281

282+
@Test
283+
void seekNearTheEndIsNotLost() throws Exception {
284+
// Two seconds of video only. All of it is decoded and queued within
285+
// moments, so the decoding thread spends the playback waiting for the
286+
// queue to play out, which is where a seek used to be mistaken for the
287+
// end of the source.
288+
try (Playback playback = new Playback(new MediaReader(
289+
Paths.get(MediaPlayerTest.class.getResource("/media-test-h264.mkv").toURI())))) {
290+
playback.player.play();
291+
292+
Thread.sleep(700);
293+
294+
int before = playback.frames.get();
295+
296+
assertTrue(before > 0 && before < 30, "frames before the seek: " + before);
297+
assertEquals(1, playback.ended.getCount(), "ended before the seek");
298+
299+
playback.player.seek(0);
300+
301+
assertTrue(playback.ended.await(15, TimeUnit.SECONDS), "no end of stream");
302+
303+
// The seek went back to the start, so the whole source plays again
304+
// on top of what was delivered before it.
305+
assertTrue(playback.frames.get() >= before + 29,
306+
"playback did not go on after the seek: " + playback.frames.get()
307+
+ " frames, " + before + " before it");
308+
assertEquals(MediaPlayerState.ENDED, playback.player.getState());
309+
}
310+
}
311+
312+
@Test
313+
void positionFollowsABackwardSeekOfAudio() throws Exception {
314+
// Audio alone, where nothing but the audio sets the position.
315+
Path flac = TestMedia.constantFlac(tempDir, 48000, 40, (short) 1000);
316+
317+
try (Playback playback = new Playback(new MediaReader(flac))) {
318+
playback.player.play();
319+
320+
Thread.sleep(1500);
321+
322+
long before = playback.player.getPositionUs();
323+
324+
assertTrue(before > 1_000_000, "position before the seek: " + before);
325+
326+
playback.player.seek(0);
327+
328+
Thread.sleep(400);
329+
330+
long after = playback.player.getPositionUs();
331+
332+
// About 0.4 s into the source again, not held at where it was.
333+
assertTrue(after < before / 2, "position after seeking back: " + after
334+
+ " us, " + before + " us before it");
335+
}
336+
}
337+
282338
@Test
283339
void needsAtLeastOneSource() throws Exception {
284340
MediaReader reader = new MediaReader(asset());

0 commit comments

Comments
 (0)