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

HLS support for trick play with EXT-X-I-FRAMES-ONLY and EXT-X-I-FRAME-STREAM-INF #474

Closed
Arqu opened this issue May 21, 2015 · 32 comments
Closed
Assignees

Comments

@Arqu
Copy link

Arqu commented May 21, 2015

I can't seem to find any reference in the code to these tags and would like to know if this could be implemented as it would allow more advanced control of the video stream. This would basically lift the ExoPlayer to EXT-X-VERSION 4 of HLS.

@ojw28
Copy link
Contributor

ojw28 commented May 21, 2015

Using I-Frame only variants looks pretty optional to me. Marking as enhancement, but probably wont happen any time soon.

@Arqu
Copy link
Author

Arqu commented May 21, 2015

Yes, the I-Frame only is not really required for stream playback and core functionality, however it allows us to play in fast forward and reverse which can enhance a users experience. It is even referred to as a key feature by Apple.

@Arqu
Copy link
Author

Arqu commented Jun 3, 2015

I've started to work on this, once there is a substantial amount I'll submit it. Currently I've just added parsing of the media playlist tags, but made sure none of them are processed and executed until this is implemented.

However, I wanted to ask how should I approach implementing playback in this case, i.e. only play those I-FRAMES and play them in some term of I-FRAMES/second instead of the usual FPS? Maybe even expose this playback speed manipulation so that you can pretty much do 2x 4x 8x... fast forward.

@Arqu
Copy link
Author

Arqu commented Jun 5, 2015

I've found out, that if you simply open up an I-FRAME list, there are two things that happen:

  1. It starts playing - thus playback is more or less possible, I just had to play around a little bit with timing and render it immediately to the screen.
  2. It gets stuck on the first chunk and keeps reloading it infinitely. Seems like it reads the bytes ok but somewhere along the way the extractor gets stuck and never gets prepared thus throwing the player into an infinite prepare loop.

So any thoughts about case 2? I've spent all day tracking down where it breaks...

@Arqu
Copy link
Author

Arqu commented Jun 11, 2015

I've managed to get a lot of things going regarding this and most of the code is already up on: https://github.com/Arqu/ExoPlayer/tree/dev however I wouldn't want to merge it yet as it is only basic support for directly playing back IFRAME media playlists (It is not really specified that way in the HLS spec but it helps to get things going). However to get this thing done I need to manage a few more things, one is #531 and the other is how to allow switching to an IFRAME track. I've also added some basic code to indicate if a variant has an IFRAME playlist associated with it, but I still have to get down to the part of managing if we are playing the full stream or the IFRAME playlist. So if somebody could help out a bit, especially with the buffering part, that would be great.

EDIT:
There is something else regarding IFRAME playback, I found out that I had an issue playing Apple's samples of IFRAME playlists and investigated a bit. Found out that in their streams, the IFRAME list doesn't have a single chunk with the PID 0 indicating the required table for demuxing streams therefore it never plays. After further analysis I found that their full video streams have a first chunk with PID 0 and the following are what ever comes from the table, so to implement switching between IFRAME playback and normal playback, we need to actually keep using the same extractor with the correct mappings of PMT and PAT indexes from the original stream and then switch the source of the chunks to the IFRAME list (like when the user clicks on fast forward).

However to get this going I first need to solve the problems above to make IFRAME playback in a sensible manner even possible.

@Arqu Arqu changed the title HLS support for EXT-X-I-FRAMES-ONLY and EXT-X-I-FRAME-STREAM-INF HLS support for trick play with EXT-X-I-FRAMES-ONLY and EXT-X-I-FRAME-STREAM-INF Jun 11, 2015
@deepvernekar
Copy link

Hi,
Currently i am working on HLS4.0 trick play. i just parsed the i-frame playlist. also i was trying to play those segments. Initially due to byte range offset i was getting few errors and found that offset value will be skips reading PMT/PAT. After that i removed the byterange param and played the segment and found playing perfect. I need to understand more about intraframes as well as byte range. can you please explain me about that. Let me know if you have any good code example for the same.

@n2lovell
Copy link

I'm currently planning to implement this in a partial c++ port of exoplayer (ported most of the manifest module layer + fmp4 extractors).

I'm currently thinking about making the I-Frames track(s) as a special type of track that the player explicitly switches to.

General idea: When entering a trick mode, flush the sample queue(s) and then rapidly seek around the special track (number of i-frames seeked to can be determined based upon the current network rate). Switching back is "trivial" at least for segment level seeks (I haven't looked at Exo's support for seeks within a segment, the port's handling of the sample queue is outside of the manifest modules).

  1. Expose the special tracks like media playlists, but marked as trick mode tracks.
  2. Have the track selector interface get an explicit argument to switch to the trick mode tracks.
    As a result of 1 +2: the chunkSources can then treat the selection mostly as just a track change (conceivably it is just a track change, but I've heard some things that potentially make seeking to the live point problematic -- so its likely that it might have to be able to terminate the trick mode if it can't fulfill the seek)

I'm interested in feedback on the design because I'm expecting that at some point, I can get another team to switch to using mostly mainline Exoplayer for their Android playback. In which case, I'd like to get a similar code change done and attempt to push the change upstream.

@ojw28
Copy link
Contributor

ojw28 commented Sep 12, 2018

That sounds along the right lines. I'd probably do it as follows, which I think is similar to what you're suggesting:

  • Expose the i-frame track as a special track in its own TrackGroup. A good first step would just be getting this working as a track that can be played. This should be fairly straightforward.
  • Add a new C.SelectionFlags constant to indicate the track is for trick play, and set it on the Format for the trick play track. This should also be straightforward.
  • Add a Player.setTrickPlayEnabled, which triggers a track re-selection internally. TrackSelector.selectTracks would need to be modified to take an additional boolean indicating whether the selection is for trick play mode.

At this point you probably have something that more or less works, although you may want to make some further tweaks to make things better, like:

  • If seeks arrive so quickly that each new seek is received before a frame is loaded at the previous position, then I think the player can end up rendering nothing. To fix this, when in trick play mode, it probably makes sense to defer processing seeks if a frame has yet to be rendered after the previous seek. Then when that happens, discard all but the last pending seek, and process that.
  • Preventing playback from proceeding whilst in trick play mode, even if playWhenReady is true.
  • You may want to have tell the Renderers that they're in trick play mode, in case they want to do any kind of special behavior. I'm not sure what that would be though :).

@n2lovell
Copy link

Just to report back, I finally got around to implementing this recently in the port (took ~1 week). Through to the sampleQueues it was pretty simple to implement, after that the HAL layer I used did the rest.

For a clarification as what I meant when I said seek. I think the closest approximation in Exo is changing the nextLoadPosition so that instead of loading the very next segment, instead load N seconds beyond the last load position (this prevents having to download every single i-frame listed in the playlist and instead only loads the ones that will be displayed).

Skipping frames definitely has consequences though, since it requires a flush for every rate change (with the design the port uses) -- in practice I've found that it hasn't mattered though. If you choose not to skip i-frames, then you need to discard frames before sending them to the decoder.

After that it was more or less cheating given that the HAL layer I use already supported trickmodes directly within the decoder. I.e. I just dumped the frames into the decoder, and told it to run at 8fps.

The sequence in the port was roughly:
User enters i-frame based trickmode
-- SelectTracks is triggered with forceReset
-- Causing Flush queues/decoder
-- Reconfigure decoder. Had to propagate a flag to the renderer to say to enter a new mode.
-- ChangeNextLoadPos to skip i-frames based on expected rate

@bkodirov
Copy link

@n2lovell Do you have a PR for that?
I'm interested on the implementation.

@stevemayhew
Copy link
Contributor

Oliver's comment seems the best path forward, to treat the track as "special" and use trackselection to activate it.

Other players that implement trickplay with the iFrame playlist treat it basically as a level down, switching to it when the bandwidth required exceeds the matching current spacial resolution's lowest bitrate * the playback rate, for practical purposes this is less then 5-8x.

We will start with TrackGroup / TrackSelector implementation, as that is needed regardless. I do suspect code will need to work with the seek and renderer logic to prioritizing rendering some frame[s] as playback progresses vs achieving a target frame rate.

@stevemayhew
Copy link
Contributor

stevemayhew commented Jul 18, 2019

Oliver (@ojw28), please reference the design document here: https://drive.google.com/file/d/1_efCflJ-OfmRgu03qn1MbqQ7wsNYozj-/view?usp=sharing Let me know if anyone else wants to be involved in the effort please feel free to comment

I am close to having code (ExoPlayer code branched from dev-v2) to share that will publish an Iframe stream that will render with MediaCodecVideoRenderer.

Please have a look at the design and let me know if the path is acceptable, I can include a Markdown version of the document with the pull request.

@ojw28
Copy link
Contributor

ojw28 commented Jul 23, 2019

Thanks a lot for the doc! As an aside for possible future reference (I wouldn't bother converting what you've done or anything :)), using Google Docs is pretty nice for letting people comment on specific parts of a doc, and/or potentially co-editing.

A few comments:

IFrameVariant

It's probably OK just to use the regular Variant class with the @Nullable fields set to null (except possibly videoGroupId), unless there's a particularly compelling reason for a new class.

except there are no other Renditions possible from it

It's possible to set a VIDEO= attribute on an EXT-X-I-FRAME-STREAM-INF. It seems it's required to do this in the case of alternate video renditions. This is not something we support properly currently, but we'd like to at some point.

is HlsMediaPeriod will expose the IFrameVariant as a TrackGroup

In the case of multiple IFrameVariants, I assume the proposal is to create a single TrackGroup containing all of them?

Some more general feedback:

  1. I'm not sure, but thinking about this a bit more, I wonder whether i-frame tracks should just go into the existing video TrackGroup. We don't otherwise split different frame-rates into their own track groups (e.g. 30fps / 60fps), and an i-frame only track is kinda just a track that happens to have a very low frame-rate. Putting them in the existing TrackGroup allows for the possibility of a TrackSelection implementation that adapts between regular and i-frame only tracks, which seems like a nice thing to allow even if that's not what happens by default. Having a separate TrackGroup makes this impossible, which seems like an unnecessary constraint to impose. I think this is definitely a good argument for streams where audio and video are delivered separately (i.e. DASH, SmoothStreaming, and HLS specifically when audio is split out). It becomes a lot more complicated when there can be audio muxed in with the video though :(. Any thoughts? If it's uncertain what's best to do here, we should probably design things to work both ways so that we can easily adjust to one way or another of doing this later on.
  2. I think i-frame only tracks should still be TRACK_TYPE_VIDEO. A ROLE_FLAG_TRICK_PLAY or (possibly more accurate) ROLE_FLAG_I_FRAME_ONLY value for Format.RoleFlags seems like an appropriate way to mark tracks that are i-frame only.
  3. As you allude to, renderers might want to do something differently when playing a trick play track. It would be preferable if this could be done dynamically by video renderers, by looking at the RoleFlags of the current input format. This would be necessary to support normal track <-> i-frame track adaptation.

@stevemayhew
Copy link
Contributor

stevemayhew commented Jul 23, 2019 via email

@ojw28
Copy link
Contributor

ojw28 commented Jul 23, 2019

Single iFrame track should support different play rates, the trick is how it is rendered ;-).

I'm not sure how that addresses my comment about putting regular and i-frame only tracks into the same TrackGroup. Please clarify.

@stevemayhew
Copy link
Contributor

stevemayhew commented Jul 24, 2019 via email

@ojw28
Copy link
Contributor

ojw28 commented Jul 25, 2019

One thing to consider, the i-Frame only track not only lowers the frame rate but also drops information (that is all the frames between the IDR are dropped)

Is that logically any different to dropping every other frame in the case of a 30fps track where 60fps tracks are available (other than being far more extreme)? Putting the i-frame only tracks in the same TrackGroup as regular video tracks doesn't imply that adaptation between them will occur. The TrackSelector (and TrackSelections) determine which tracks from the group are used and how adaptation between them occurs. By default I'd propose i-frame only tracks are not considered. The point is that if they're in the same TrackGroup it will be possible for a TrackSelection to enable adaptation to i-frame tracks, if it wants to. Where-as if they're no, this wont be possible.

Interesting thought, I need to verse myself more deeply in TrackSelection, is there anything beyond this: https://exoplayer.dev/track-selection.html

The JavaDoc for the track selection classes is reasonably good, so you may find that helpful as well.

The I-Frame track never includes muxed audio

Yes, this may be a problem for the HLS case where audio is muxed.

@stevemayhew
Copy link
Contributor

One thing to consider, the i-Frame only track not only lowers the frame rate but also drops information (that is all the frames between the IDR are dropped)

Is that logically any different to dropping every other frame in the case of a 30fps track where 60fps tracks are available (other than being far more extreme)? Putting the i-frame only tracks in the same TrackGroup as regular video tracks doesn't imply that adaptation between them will occur...

Thanks... The more I look at it the more it makes sense. The overhead to run an additional HlsSampleStreamWrapper just for the I-Frame only playlists seems wasted. Interestingly the FRAME-RATE attribute is not allowed on I-Frame only playlist, it can be inferred from the durations in the sub-playlist. So we will want a role or some other Format attribute to mark them.

The point is that if they're in the same TrackGroup it will be possible for a TrackSelection to enable adaptation to i-frame tracks, if it wants to. Where-as if they're no, this wont be possible.

I'm on board now, yes.. I think using adaptation to switch to i-frame tracks based on some criteria is indeed the correct way to go.

The I-Frame track never includes muxed audio

Yes, this may be a problem for the HLS case where audio is muxed.

When you adapt to an I-Frame only track audio and captions are no longer linked. These are not allowed attributes as per the HLS spec (https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#page-35) not sure about DASH. Will look into if adaption changes allow for dropping audio and caption render.

@stevemayhew
Copy link
Contributor

Pull request 6270 will address this issue.

@stevemayhew
Copy link
Contributor

#6270

@lupin4th
Copy link

@stevemayhew
How can I playback test with i frame track on the demo app?

@stevemayhew
Copy link
Contributor

stevemayhew commented Sep 27, 2019 via email

@lupin4th
Copy link

lupin4th commented Oct 2, 2019

@stevemayhew
Thank you for your answer.
It works well.
Is i-frame track playback possible at 4x, 2x?

@stevemayhew
Copy link
Contributor

stevemayhew commented Oct 2, 2019 via email

@lupin4th
Copy link

lupin4th commented Oct 6, 2019

@stevemayhew
I use apple's sample url(https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8).

As you said I turn off Audio

mTrackSelector.setParameters(
				mTrackSelector
						.buildUponParameters()
						.clearSelectionOverrides(1)
						.setRendererDisabled(1, true));

and Set playback speed

PlaybackParameters playbackParameters = new PlaybackParameters(16.0f);
    player.setPlaybackParameters(playbackParameters);

Then I checked selectedPlaylistUrl in getNextChunk method(HlsChunkSource.java).

However, it has not been changed to i-frame url.

@lupin4th
Copy link

lupin4th commented Oct 7, 2019

I changed adapativeTrackSelection canSelectFormat method

protected boolean canSelectFormat(
      Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) {
    boolean isNonIframeOnly = (format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) == 0;
    boolean canSelect = playbackSpeed < 8;

    return canSelect && isNonIframeOnly;  
  }

As above, the selectedPlaylistUrl changes when the speed is more than 8x.

However, error occurs as shown below.

ava.lang.IllegalArgumentException
        at com.google.android.exoplayer2.util.Assertions.checkArgument(Assertions.java:39)
        at com.google.android.exoplayer2.upstream.DataSpec.<init>(DataSpec.java:248)
        at com.google.android.exoplayer2.upstream.DataSpec.subrange(DataSpec.java:336)
        at com.google.android.exoplayer2.upstream.DataSpec.subrange(DataSpec.java:322)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.feedDataToExtractor(HlsMediaChunk.java:346)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.loadMedia(HlsMediaChunk.java:325)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.load(HlsMediaChunk.java:301)
        at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:381)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
2019-10-10 11:32:00.681 32237-32237/com.google.android.exoplayer2.demo E/EventLogger: internalError [15.42, 60.86, window=0, period=0, loadError]
    java.io.EOFException
        at com.google.android.exoplayer2.extractor.DefaultExtractorInput.readFromDataSource(DefaultExtractorInput.java:265)
        at com.google.android.exoplayer2.extractor.DefaultExtractorInput.readFully(DefaultExtractorInput.java:72)
        at com.google.android.exoplayer2.extractor.DefaultExtractorInput.readFully(DefaultExtractorInput.java:81)
        at com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.readSample(FragmentedMp4Extractor.java:1293)
        at com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.read(FragmentedMp4Extractor.java:330)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.feedDataToExtractor(HlsMediaChunk.java:357)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.loadMedia(HlsMediaChunk.java:325)
        at com.google.android.exoplayer2.source.hls.HlsMediaChunk.load(HlsMediaChunk.java:301)
        at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:381)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

@2wchuang

This comment has been minimized.

@stevemayhew
Copy link
Contributor

@stevemayhew
Thank you for your answer.
It works well.
Is i-frame track playback possible at 4x, 2x?

Depends on the density of I-Frames in the track, the compression of the I-Frame, network bandwidth, frame rate the codec hardware you have will support.

If you have 2 second GOP then the base rate would be .5fps, so even 30x would be 15fps, very possible.

@stevemayhew
Copy link
Contributor

@2wchuang reverse is possible (in theory), however the current PlaybackParameters restricts this with an assert (wisely so, as it will take rework to support this). For normal streams (mix I/B/P with audio) reverse playback is practically impossible. With an IFRAME only stream (and only rendering video) it is certainly possible, we are investigating a design for this but it will take changes to the Renderer, SampleStream API (needs playback speed/direction) and the sample stream implementations need to be able to deal with:

  1. PTS appearing in reverse order.
  2. Fetching chunks from a playlist in-reverse order
  3. Buffering a back buffer

In short, no small order.

icbaker pushed a commit that referenced this issue Apr 15, 2020
Issue: #6054
Issue: #474
PiperOrigin-RevId: 306437452
icbaker pushed a commit that referenced this issue Apr 15, 2020
Issue: #6054
Issue: #474
PiperOrigin-RevId: 306504362
ojw28 added a commit that referenced this issue May 28, 2020
Issue: #6054
Issue: #474
PiperOrigin-RevId: 306437452
ojw28 added a commit that referenced this issue May 28, 2020
Issue: #6054
Issue: #474
PiperOrigin-RevId: 306504362
zxzx74147 pushed a commit to zxzx74147/ExoPlayer that referenced this issue Sep 7, 2020
zxzx74147 pushed a commit to zxzx74147/ExoPlayer that referenced this issue Sep 7, 2020
Issue: google#6054
Issue: google#474
PiperOrigin-RevId: 306504362
@AquilesCanta
Copy link
Contributor

This is already fully supported.

I think this bug should have been marked as fixed since the last commit (2b1ec5c). Since then, fmp4 i-frame-only playback was broken, for which I filed #7512. #7512 has also been fixed recently.

@stevemayhew
Copy link
Contributor

stevemayhew commented Dec 11, 2020 via email

@SteveCrichton
Copy link

@stevemayhew

First of all, thanks for your work on this issue. I thought I was going to have to generate mp4 files which contain only a single I-Frame and then manually load those into the player at intervals in order to enable scrubbing when fast forwarding and rewinding. Doable but tedious to keep everything synchronized with my actual stream. This is much cleaner.

You issue repeated seeks to play reverse, you can stay paused (play when ready set false). Turn off sound and subtitles then select the iFrame track with an override

Your suggestion for reverse playback makes sense and I have implemented it for enabling this functionality for fmp4 i-frame-only tracks. (I am outputting 1 fps fmp4 variant streams in 6 second chunks which contains only IDR frames from my encoder as it doesn’t support outputting i-frame-only variants, running those through a processor which extracts the 6 samples in each chunk and breaks them into 6 individual fragments each of which contains a single sample, and then dynamically generating media playlists for the i-frame-only streams and adding the appropriate tags to the master playlists).

One thing I’m seeing is that the player is doing a lot of unnecessary buffering when I am “rewinding”. Every time I seek back (while paused) it buffers as if it is going to play the i-frame track from the new position (which will never happen because I will switch back to my “normal” variant before playing again). This all makes sense because the player doesn’t know what I’m doing but it is impacting performance both on the server side and the client side (and the network in between for that matter). My biggest concern is the client side as the player doesn’t return to the ready to play state and display the i-frame I’m seeking to until it has built a buffer (that I know it is never going to use) and this sometimes doesn't complete before I want to seek back again.

I implemented a custom load control which allows me to modify the buffer parameters on the fly (I reduce the thresholds when performing reverse playback) but have had only limited success. If I set the buffer to be too short the player never returns to the ready to play state. In fact, it never even calls the shouldStartPlayback method of my load control. (I think this may have something to do with imperfect alignment between the playlists for my i-frame-only variants my “normal” variants but I haven’t dug into that theory yet).

I’m thinking that the next thing I’ll try is to implement a caching data source so that the player doesn’t have to go back to the server to get the same chunks over and over (if I seek back 1s then it would only need to get one more chunk) but I know this isn’t going to improve things much if I start seeking back several seconds at a time and thought I'd ask if you might be willing to share some advice before I continue.

Any suggestions would be much appreciated.

@google google locked and limited conversation to collaborators Feb 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

10 participants