Skip to content

Commit c7e00b1

Browse files
icbakercopybara-github
authored andcommitted
Add fps-awareness to DefaultTrackSelector
This change aims to prioritise tracks that have a 'smooth enough for video' frame rate, without always selecting the track with the highest frame rate. In particular MP4 files extracted from motion photos sometimes have two HEVC tracks, with the higher-res one having a very low frame rate (not intended for use in video playback). Before this change `DefaultTrackSelector` would pick the low-fps, high-res track. This change adds a somewhat arbitrary 10fps threshold for "smooth video playback", meaning any tracks above this threshold are selected in preference to tracks below it. Within the tracks above the threshold other attributes are used to select the preferred track. We deliberately don't pick the highest-fps track (over pixel count and bitrate), because most users would prefer to see a 30fps 4k track over a 60fps 720p track. This change also includes a test MP4 file, extracted from the existing `jpeg/pixel-motion-photo-2-hevc-tracks.jpg` file by logging `mp4StartPosition` in [`MotionPhotoDescription.getMotionPhotoMetadata`](https://github.com/androidx/media/blob/b930b40a16c06318e43c81771fa2b1024bdb3f29/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/MotionPhotoDescription.java#L123) and then using `dd`: ``` mp4StartPosition=2603594 $ dd if=jpeg/pixel-motion-photo-2-hevc-tracks.jpg \ of=mp4/pixel-motion-photo-2-hevc-tracks.mp4 \ bs=1 \ skip=2603594 ``` ---- This solution is in addition to the `JpegMotionPhotoExtractor` change made specifically for these two-track motion photos in androidx@5266c71. We will keep both changes, even though that change is not strictly needed after this one, because adding the role flags helps to communicate more clearly the intended usage of these tracks. This change to consider FPS seems like a generally useful improvement to `DefaultTrackSelector`, since it seems unlikely we would prefer a 5fps video track over a 30fps one. Issue: androidx#1051 PiperOrigin-RevId: 611015459
1 parent 626a8ad commit c7e00b1

File tree

6 files changed

+519
-2
lines changed

6 files changed

+519
-2
lines changed

RELEASENOTES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
* Add support for changing between SDR and HDR input media in a sequence.
1919
* Add support for composition-level audio effects.
2020
* Track Selection:
21+
* `DefaultTrackSelector`: Prefer video tracks with a 'reasonable' frame
22+
rate (>=10fps) over those with a lower or unset frame rate. This ensures
23+
the player selects the 'real' video track in MP4s extracted from motion
24+
photos that can contain two HEVC tracks where one has a higher
25+
resolution but a very small number of frames
26+
([#1051](https://github.com/androidx/media/issues/1051)).
2127
* Extractors:
2228
* Audio:
2329
* Allow renderer recovery by disabling offload if audio track fails to

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3516,6 +3516,12 @@ public TrackInfo(int rendererIndex, TrackGroup trackGroup, int trackIndex) {
35163516

35173517
private static final class VideoTrackInfo extends TrackInfo<VideoTrackInfo> {
35183518

3519+
/**
3520+
* Frame rate below which video playback will definitely not be considered smooth by the human
3521+
* eye.
3522+
*/
3523+
private static final float MIN_REASONABLE_FRAME_RATE = 10;
3524+
35193525
public static ImmutableList<VideoTrackInfo> createForTrackGroup(
35203526
int rendererIndex,
35213527
TrackGroup trackGroup,
@@ -3551,6 +3557,12 @@ public static ImmutableList<VideoTrackInfo> createForTrackGroup(
35513557
private final Parameters parameters;
35523558
private final boolean isWithinMinConstraints;
35533559
private final boolean isWithinRendererCapabilities;
3560+
3561+
/**
3562+
* True if {@link Format#frameRate} is set and is at least {@link #MIN_REASONABLE_FRAME_RATE}.
3563+
*/
3564+
private final boolean hasReasonableFrameRate;
3565+
35543566
private final int bitrate;
35553567
private final int pixelCount;
35563568
private final int preferredMimeTypeMatchIndex;
@@ -3599,6 +3611,8 @@ public VideoTrackInfo(
35993611
|| format.bitrate >= parameters.minVideoBitrate);
36003612
isWithinRendererCapabilities =
36013613
isSupported(formatSupport, /* allowExceedsCapabilities= */ false);
3614+
hasReasonableFrameRate =
3615+
format.frameRate != Format.NO_VALUE && format.frameRate >= MIN_REASONABLE_FRAME_RATE;
36023616
bitrate = format.bitrate;
36033617
pixelCount = format.getPixelCount();
36043618
preferredRoleFlagsScore =
@@ -3669,16 +3683,19 @@ private static int compareNonQualityPreferences(VideoTrackInfo info1, VideoTrack
36693683
.compare(info1.preferredRoleFlagsScore, info2.preferredRoleFlagsScore)
36703684
// 2. Compare match with implicit content preferences set by the media.
36713685
.compareFalseFirst(info1.hasMainOrNoRoleFlag, info2.hasMainOrNoRoleFlag)
3672-
// 3. Compare match with technical preferences set by the parameters.
3686+
// 3. Compare match with 'reasonable' frame rate threshold.
3687+
.compareFalseFirst(info1.hasReasonableFrameRate, info2.hasReasonableFrameRate)
3688+
// 4. Compare match with technical preferences set by the parameters.
36733689
.compareFalseFirst(info1.isWithinMaxConstraints, info2.isWithinMaxConstraints)
36743690
.compareFalseFirst(info1.isWithinMinConstraints, info2.isWithinMinConstraints)
36753691
.compare(
36763692
info1.preferredMimeTypeMatchIndex,
36773693
info2.preferredMimeTypeMatchIndex,
36783694
Ordering.natural().reverse())
3679-
// 4. Compare match with renderer capability preferences.
3695+
// 5. Compare match with renderer capability preferences.
36803696
.compareFalseFirst(info1.usesPrimaryDecoder, info2.usesPrimaryDecoder)
36813697
.compareFalseFirst(info1.usesHardwareAcceleration, info2.usesHardwareAcceleration);
3698+
36823699
if (info1.usesPrimaryDecoder && info1.usesHardwareAcceleration) {
36833700
chain = chain.compare(info1.codecPreferenceScore, info2.codecPreferenceScore);
36843701
}

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public static ImmutableList<String> mediaSamples() {
4646
"midroll-5s.mp4",
4747
"postroll-5s.mp4",
4848
"preroll-5s.mp4",
49+
"pixel-motion-photo-2-hevc-tracks.mp4",
4950
"sample_ac3_fragmented.mp4",
5051
"sample_ac3.mp4",
5152
"sample_ac4_fragmented.mp4",

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,6 +2821,84 @@ public void selectTracks_withPreferredAudioMimeTypes_selectsTrackWithPreferredMi
28212821
assertFixedSelection(result.selections[0], trackGroups, formatAac);
28222822
}
28232823

2824+
/**
2825+
* Tests that the track selector will select a group with a single video track with a 'reasonable'
2826+
* frame rate instead of a larger groups of tracks all with lower frame rates (the larger group of
2827+
* tracks would normally be preferred).
2828+
*/
2829+
@Test
2830+
public void selectTracks_reasonableFrameRatePreferredOverTrackCount() throws Exception {
2831+
Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon();
2832+
Format frameRateTooLow = formatBuilder.setFrameRate(5).build();
2833+
Format frameRateAlsoTooLow = formatBuilder.setFrameRate(6).build();
2834+
Format highEnoughFrameRate = formatBuilder.setFrameRate(30).build();
2835+
// Use an adaptive group to check that frame rate has higher priority than number of tracks.
2836+
TrackGroup adaptiveFrameRateTooLowGroup = new TrackGroup(frameRateTooLow, frameRateAlsoTooLow);
2837+
TrackGroupArray trackGroups =
2838+
new TrackGroupArray(adaptiveFrameRateTooLowGroup, new TrackGroup(highEnoughFrameRate));
2839+
2840+
TrackSelectorResult result =
2841+
trackSelector.selectTracks(
2842+
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
2843+
2844+
assertFixedSelection(result.selections[0], trackGroups, highEnoughFrameRate);
2845+
}
2846+
2847+
/**
2848+
* Tests that the track selector will select the video track with a 'reasonable' frame rate that
2849+
* has the best match on other attributes, instead of an otherwise preferred track with a lower
2850+
* frame rate.
2851+
*/
2852+
@Test
2853+
public void selectTracks_reasonableFrameRatePreferredButNotHighestFrameRate() throws Exception {
2854+
Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon();
2855+
Format frameRateUnsetHighRes =
2856+
formatBuilder.setFrameRate(Format.NO_VALUE).setWidth(3840).setHeight(2160).build();
2857+
Format frameRateTooLowHighRes =
2858+
formatBuilder.setFrameRate(5).setWidth(3840).setHeight(2160).build();
2859+
Format highEnoughFrameRateHighRes =
2860+
formatBuilder.setFrameRate(30).setWidth(1920).setHeight(1080).build();
2861+
Format highestFrameRateLowRes =
2862+
formatBuilder.setFrameRate(60).setWidth(1280).setHeight(720).build();
2863+
TrackGroupArray trackGroups =
2864+
new TrackGroupArray(
2865+
new TrackGroup(frameRateUnsetHighRes),
2866+
new TrackGroup(frameRateTooLowHighRes),
2867+
new TrackGroup(highestFrameRateLowRes),
2868+
new TrackGroup(highEnoughFrameRateHighRes));
2869+
2870+
TrackSelectorResult result =
2871+
trackSelector.selectTracks(
2872+
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
2873+
2874+
assertFixedSelection(result.selections[0], trackGroups, highEnoughFrameRateHighRes);
2875+
}
2876+
2877+
/**
2878+
* Tests that the track selector will select a track with {@link C#ROLE_FLAG_MAIN} with an
2879+
* 'unreasonably low' frame rate, if the other track with a 'reasonable' frame rate is marked with
2880+
* {@link C#ROLE_FLAG_ALTERNATE}. These role flags show an explicit signal from the media, so they
2881+
* should be respected.
2882+
*/
2883+
@Test
2884+
public void selectTracks_roleFlagsOverrideReasonableFrameRate() throws Exception {
2885+
Format.Builder formatBuilder = VIDEO_FORMAT.buildUpon();
2886+
Format mainTrackWithLowFrameRate =
2887+
formatBuilder.setFrameRate(3).setRoleFlags(C.ROLE_FLAG_MAIN).build();
2888+
Format alternateTrackWithHighFrameRate =
2889+
formatBuilder.setFrameRate(30).setRoleFlags(C.ROLE_FLAG_ALTERNATE).build();
2890+
TrackGroupArray trackGroups =
2891+
new TrackGroupArray(
2892+
new TrackGroup(mainTrackWithLowFrameRate),
2893+
new TrackGroup(alternateTrackWithHighFrameRate));
2894+
2895+
TrackSelectorResult result =
2896+
trackSelector.selectTracks(
2897+
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
2898+
2899+
assertFixedSelection(result.selections[0], trackGroups, mainTrackWithLowFrameRate);
2900+
}
2901+
28242902
/** Tests audio track selection when there are multiple audio renderers. */
28252903
@Test
28262904
public void selectTracks_multipleRenderer_allSelected() throws Exception {
Binary file not shown.

0 commit comments

Comments
 (0)