Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start position of 0 always resolves to default position for live playbacks #7975

Closed
DavidMihola opened this issue Sep 23, 2020 · 11 comments
Closed
Assignees
Labels

Comments

@DavidMihola
Copy link

[REQUIRED] Searched documentation and issues

Since the problem only appeared after updating from 2.11.8 to 2.12.0 I searched the recent issues for "seek" and "startPosition" but didn't find anything that seemed related.

[REQUIRED] Question

In our TV app we offer users the possibility to watch the current program from the beginning, i. e. instead of simple live stream with a short live window we have a stream with a fixed start but which grows in length every time a new segment is available on the streaming server.

Since the default position in those streams seems to be right at the live edge we have always passed the start position (simpleExoPlayer.seekTo(0)) explicitly before prepare()ing the player. Up until and including 2.11.8 this has worked without any problems.

Since updating to 2.12.0 a few days ago this "initial seek position" seems to be ignored and the player starts at the default position, at the live edge. I saw that the prepare(mediaSource, false, true) which we were using so far is now deprecated. But replacing it with

simpleExoPlayer.setMediaSource(buildMediaSource(), startPositionInMs)
simpleExoPlayer.prepare()

does not resolve the problem. Even here, it seems that the startPositionInMs is ignored.

Unfortunately, I cannot provide a public test stream to demonstrate the problem, but this is the HLS master playlist:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=1963735,PROGRAM-ID=1,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=960x540
04(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4636146,PROGRAM-ID=1,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1280x720
01(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3055720,PROGRAM-ID=1,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1280x720
02(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2340502,PROGRAM-ID=1,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1280x720
03(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1146343,PROGRAM-ID=1,CODECS="avc1.4d401e,mp4a.40.2",RESOLUTION=640x360
05(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=686561,PROGRAM-ID=1,CODECS="avc1.4d401e,mp4a.40.2",RESOLUTION=480x270
06(start=1600850100_stop=1600864500).m3u8
#EXT-X-STREAM-INF:BANDWIDTH=185753,PROGRAM-ID=1,CODECS="avc1.4d400d,mp4a.40.2",RESOLUTION=320x180
07(start=1600850100_stop=1600864500).m3u8

and this is the start of one of the variant playlists:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:400212523
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:03Z
#EXTINF:4, no desc
20200814T123515-01-400212523.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:07Z
#EXTINF:4, no desc
20200814T123515-01-400212524.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:11Z
#EXTINF:4, no desc
20200814T123515-01-400212525.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:15Z
#EXTINF:4, no desc
20200814T123515-01-400212526.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:19Z
#EXTINF:4, no desc
20200814T123515-01-400212527.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:23Z
#EXTINF:4, no desc
20200814T123515-01-400212528.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:27Z
#EXTINF:4, no desc
20200814T123515-01-400212529.ts
#EXT-X-PROGRAM-DATE-TIME:2020-09-23T08:35:31Z
#EXTINF:4, no desc
20200814T123515-01-400212530.ts
...

As I said, the variant playlists grow for three more lines every four seconds, when the next segment becomes available.

It may well be a configuration issue with our streams (this time, for once, the newly configured DASH streams work better for the same scenario, but we cannot migrate all streams from HLS to DASH at the moment) - but since it still worked with 2.11.8 I thought there might be a way to get it to work with 2.12.0.

@stevemayhew
Copy link
Contributor

@AquilesCanta I've seen this too with our 2.12.3 upgrade.

Since the MediaItem API did not exist with an initial start position we wait until the first timeline update occurs with a non-empty timeline to issue the seek. What I see is the position is overwritten with the default window start for live after the track selection. It is not reproduced 100%, and if you enable chunk less prepare, even less frequent, so almost certainly it is a timing issue.

I'm looking into the issue, I'll send you some logs to and tag this issue number.

@stevemayhew
Copy link
Contributor

This is easy to reproduce with current dev-v2...

Modify the demo player to force an initial seek:

    startWindow = 0;
    startPosition = 0;
    boolean haveStartPosition = startWindow != C.INDEX_UNSET;
    if (haveStartPosition) {
      player.seekTo(startWindow, startPosition);
    }
    player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);

You can see with some debug prints that ExoPlayer issues the seek internally when the first source refresh occurs, but then wipes it out when the MSG_PERIOD_PREPARED is processed.

For live you probably want to wait till after the prepare completes to process the pendingInitialSeekPosition

Here's the logs that show this:

Initial seek is processed:

07-06 20:35:26.893 10470 10470 D ExoPlayerImplInternal: seekTo() - positionUs: 0
07-06 20:35:26.893 10470 10517 D ExoPlayerImplInternal: Process MSG_SEEK_TO - positionUs 0
07-06 20:35:26.894 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - isPrepared: false
07-06 20:35:26.894 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - positionUs: 0 contentPositionUs: -9223372036854775807 discontinuityStartPositionUs: 0 rendererPositionUs: 0
07-06 20:35:26.899 10470 10470 D EventLogger: positionDiscontinuity [eventTime=0.02, mediaPos=0.00, window=0, reason=SEEK, PositionInfo:old [window=0, period=-1, pos=0], PositionInfo:new [window=0, period=-1, pos=0]]
07-06 20:35:26.915 10470 10517 D ExoPlayerImplInternal: handleMediaSourceListInfoRefreshed() - pndingInitialSeekPosition: com.google.android.exoplayer2.ExoPlayerImplInternal$SeekPosition@8e6fcfb requestedContentPosition: 0 positionUs: 0
07-06 20:35:26.916 10470 10517 D ExoPlayerImplInternal: resetRendererPosition() - periodPositionUs: 0
07-06 20:35:26.916 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - isPrepared: false
07-06 20:35:26.916 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - positionUs: 0 contentPositionUs: 0 discontinuityStartPositionUs: 0 rendererPositionUs: 0

Prepare starts, pending seek already processed and renderPosition is 0.

07-06 20:35:26.930 10470 10517 D ExoPlayerImplInternal: prepareInternal() - renderPositionUs: 0 pendingSeek: null seekPos: -1
07-06 20:35:26.936 10470 10470 D EventLogger: state [eventTime=0.06, mediaPos=0.00, window=0, BUFFERING]
07-06 20:35:27.005 10470 10517 D ExoPlayerImplInternal: resetRendererPosition() - periodPositionUs: 0
07-06 20:35:27.629 10470 10517 D ExoPlayerImplInternal: handleMediaSourceListInfoRefreshed() - pndingInitialSeekPosition: null requestedContentPosition: 0 positionUs: 0
07-06 20:35:27.639 10470 10470 D EventLogger: timeline [eventTime=0.76, mediaPos=0.00, window=0, period=0, periodCount=1, windowCount=1, reason=SOURCE_UPDATE
07-06 20:35:27.640 10470 10470 D EventLogger:   window [1801.80, seekable=true, dynamic=true]

Prepare completes and sets position to default live start position, overwriting the initial requested seek.

07-06 20:35:27.938 10470 10517 D ExoPlayerImplInternal: Process MSG_PERIOD_PREPARED - renderPosition: 0
07-06 20:35:28.280 10470 10517 D ExoPlayerImplInternal: handlePeriodPrepared() - reset renderPosition to: 1777776000
07-06 20:35:28.281 10470 10517 D ExoPlayerImplInternal: resetRendererPosition() - periodPositionUs: 1777776000
07-06 20:35:28.286 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - isPrepared: true
07-06 20:35:28.286 10470 10517 D ExoPlayerImplInternal: handlePositionDiscontinuity() - positionUs: 1777776000 contentPositionUs: 0 discontinuityStartPositionUs: 1777776000 rendererPositionUs: 0
07-06 20:35:28.595 10470 10470 D EventLogger: state [eventTime=1.71, mediaPos=1777.80, window=0, period=0, READY]
07-06 20:35:28.614 10470 10470 D EventLogger: isPlaying [eventTime=1.73, mediaPos=1777.80, window=0, period=0, true]

@stevemayhew
Copy link
Contributor

stevemayhew commented Jul 7, 2021

@tonihei this is caused by the fix for BLWE issue #8675

On further testing with the code from my comment above, I found only seek to 0 is ignored. Here is why:

      if (unpreparedMaskingMediaPeriod != null) {
        long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
        timeline.getPeriodByUid(unpreparedMaskingMediaPeriod.id.periodUid, period);
        long windowPreparePositionUs = period.getPositionInWindowUs() + periodPreparePositionUs;
        long oldWindowDefaultPositionUs =
            timeline.getWindow(/* windowIndex= */ 0, window).getDefaultPositionUs();
        if (windowPreparePositionUs != oldWindowDefaultPositionUs) {
          windowStartPositionUs = windowPreparePositionUs;
        }

The 0 value for the initial seek is what fails (all the test cases use non-zero). The Window#defaultPositionUs is initialized to 0 by default and the Window in the MaskingMediaPeriod was set to 0 explicitly so even though logically the if should be true it is false so windowStartPositionUs remains the default (live offset) and the seek is ignored.

See my comment here: bc9fb86#r53184789

I didn't attempt a pull request for this, as the fix will probably alter lots of assumptions the ad insertion logic has that I'm not aware of, comments on the Window#defaultPositionUs seem to imply

For now, @DavidMihola a simple work around is to seek to anything but 0, not ideal but you miss the first frame or so at worst. Note this is only an issue for live.

FWIW, seekToDefaultPosition() to recover from BLWE is not an great solution for a very large live window, you want to start at the oldest content.

@marcbaechinger
Copy link
Contributor

marcbaechinger commented Jul 8, 2021

Thanks @stevemayhew for the excellent analysis. I can repro and see the difference when using seekTo(0) vs. seekTo(1).

I agree is that the expression windowPreparePositionUs != oldWindowDefaultPositionUs always is 0 != 0 with an initial seek to 0.

oldWindowDefaultPositionUs has the defaultPositionUs of the PlaceholderTimeline when hasRealTimeline == false which is always 0. windowPreparePositionUs is derived from the what is passed to MaskingMediaSource.createPeriod(..., startPositionUs) which is 0 in all cases except when there is an initialSeek that is not equal 0. If I'm not mistaken this value in 'startPositionUs' is the default defaultPositionUs (not a typo, there is double default :)) or the initial seek position.

This boils down to your conclusion that the defaultPositionUs of the newTimeline is used when a seek to 0 is performed. This is an issue for all media sources that have a defaultStartPosition that is not 0, which, like you say, are probably live sources only.

Please note, that the long comment some lines further up, that explains the three possible cases actually state exactly that:

      ///    Note that this will override an
      //     intentional seek to zero for a window with a non-zero default position. This is
      //     unlikely to be a problem as a non-zero default position usually only occurs for live
      //     playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
      //     anyway.

Am I correct that your live stream has a live window with indefinite availability? So the assumption in the comment about seeking to zero being unlikely is not correct?

@stevemayhew
Copy link
Contributor

@marcbaechinger exactly... I've started on a test case in ExoPlayerTest to prove this, great to have this framework now since code in ExoPlayerImplInternal is pretty tough to test.

My thought from there was to look at enforcing these states for defaultPositionUs

  1. C.TIME_UNSET - no default has been set (the placeholder should have this unless an initial seek is done)
  2. 0 - durationUs - valid initial starting position

Some logic (ClippingMediaSource) checks for this already, others (notably MaskingMediaSource) do not. I get the general idea of ad insertion and timelines, but you all are in the thick of it more ;-)

Let me know if the test case would be welcome with a pull request, otherwise I have plenty to work on ;-) The seek to 1 is dirty, but it "works"

@marcbaechinger
Copy link
Contributor

marcbaechinger commented Jul 8, 2021

Ok, if you have other things to do, give me some time to look into this on our end. Not saying that a pull with unit test is not welcome. :)

I need to wrap my head around this some more. I agree that defaulting to 0 is problematic/scary but wanted to see if there are other ways to recognize that case (looking at hasRealTimeline or similar). I'm not fully understanding yet what all the implications would be either. It also seemed to work for DASH live with a seekTo(0) what I wanted to confirm/understand.

@stevemayhew
Copy link
Contributor

No worries.. I'll definitely finish up the unit test that demonstrates the bug (first step to fixing it ;-) ) this way whatever solution we decide on has an way to verify it really fixes the issue without having to find a live stream to test.

The use of C.TIME_UNSET (in fact, the origin of defaultPositionUs) comes from adding the ClippingMediaSource issue #1988 to allow setting playing "clipped" time ranges on single video

Live start offset uses this same defaultPositionUs in Window, here again C.TIME_UNSET is used to indicate the default start is "invalid". See the this commit 856c2f8

@stevemayhew
Copy link
Contributor

@marcbaechinger I added to the pull request #9174 with a couple of choices for unit and functional (EPII level) tests.

My gut feels it is best pushed to the unit test level and re-work the way the logic in the MaskingMediaPeriod and MaskingMediaSource use preparePositionOverrideUs and preparePositionUs to keep the initial seek/prepare start position until the timeline updates.

The logic in EPII looks like it already has a few too many complicated use cases. Is there a spec around for how the playlist transition from one to the next MediaSource is supposed to propagate position?

@stevemayhew
Copy link
Contributor

PS I suspect the reason DASH works is tied up in how the HLS playlist updates, so possibly we could push the fix down into HLS somehow, I've not looked at that yet

@tonihei tonihei changed the title Start position is ignored for some HLS streams Start position of 0 always resolves to default position for live playbacks Sep 29, 2021
@tonihei
Copy link
Collaborator

tonihei commented Sep 29, 2021

Generalized this bug to also track the problem described in #9386, which has the same root cause.

The root cause (as identified above) is that MaskingMediaSource/MaskingMediaPeriod use the value of 0 for both the literal 0 and to identify the default position. We attempt to disambiguate by make informed guesses here, but explicitly seeking to 0 in a live stream or enabling a track selection after seeking to 0 will revert back to using the default position of the live stream.

There is a known workaround to use seek(1) instead.

The potential fix will need to introduce C.TIME_UNSET to MediaSource.createPeriod and MediaPeriod.prepare methods to clearly identify the default position and adapting implementations and callers of the methods to handle this case correctly.

#9386 and #9174 contain unit tests we can use once we submit a fix for this problem.

@stevemayhew
Copy link
Contributor

Exactly, the seekTo(1) is fine for us. I agree with your suggestion for the fix, looked more involved then I wanted to attempt in a pull request

ptsekov added a commit to mediahub-bg/ExoPlayer that referenced this issue May 31, 2022
…playing at the beginning

Use this factory to workaround the following issue:
google#7975
stevemayhew referenced this issue Jul 25, 2023
MaskingMediaSource needs to resolve the prepare position set for a MaskingPeriod
while the source was still unprepared to the first actual prepare position.

It currently assumes that the period-window offset and the default position is
zero. This assumption is correct when a PlaceholderTimeline is used, but it
may not be true if the real timeline is already known (e.g. when re-preparing
a live stream after a playback error).

Fix this by using the known timeline at the time of the preparation.
Also:
 - Update a test that should have caught this to use lazy re-preparation.
 - Change the demo app code to use the recommended way to restart playback
   after a BehindLiveWindowException.

Issue: #8675
PiperOrigin-RevId: 361604191
stevemayhew added a commit to stevemayhew/media that referenced this issue Oct 20, 2023
Using 0 as the unset prepare position is the root cause of a number of issues,
as outliine in the ExoPlayer issue google/ExoPlayer#7975

The premise of this fix is that once the prepare override is used (the initial call
to `selectTracks()`) it is never needed again, so simply invalidate it after use.
stevemayhew added a commit to stevemayhew/media that referenced this issue Oct 22, 2023
Using 0 as the unset prepare position is the root cause of a number of issues,
as outliine in the ExoPlayer issue google/ExoPlayer#7975

The premise of this fix is that once the prepare override is used (the initial call
to `selectTracks()`) it is never needed again, so simply invalidate it after use.
tonihei pushed a commit to stevemayhew/media that referenced this issue Dec 12, 2023
Using 0 as the unset prepare position is the root cause of a number of issues,
as outliine in the ExoPlayer issue google/ExoPlayer#7975

The premise of this fix is that once the prepare override is used (the initial call
to `selectTracks()`) it is never needed again, so simply invalidate it after use.
tonihei pushed a commit to stevemayhew/media that referenced this issue Dec 13, 2023
Using 0 as the unset prepare position is the root cause of a number of issues,
as outliine in the ExoPlayer issue google/ExoPlayer#7975

The premise of this fix is that once the prepare override is used (the initial call
to `selectTracks()`) it is never needed again, so simply invalidate it after use.
@tonihei tonihei closed this as completed Jan 29, 2024
@google google locked and limited conversation to collaborators Mar 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

7 participants