Skip to content

Support variable length sgpd boxes in fMPEG#3243

Merged
copybara-service[bot] merged 3 commits into
androidx:mainfrom
dimitry-unified-streaming:fix-variable-length-sgpd-main-1
Jun 11, 2026
Merged

Support variable length sgpd boxes in fMPEG#3243
copybara-service[bot] merged 3 commits into
androidx:mainfrom
dimitry-unified-streaming:fix-variable-length-sgpd-main-1

Conversation

@dimitry-unified-streaming

Copy link
Copy Markdown

For sgpd boxes with version >= 1, the default_length field is zero if the length of the following sample group entries is variable.

Since the FragmentedMp4Extractor only supports one entry, skip the description_length field if applicable (e.g. when version >= 1 and default_length == 0), instead of throwing an error.

For parsing the following CencSampleEncryptionInformationGroupEntry, this should make no difference.

Issue: #3177

"Variable length description in sgpd found (unsupported)");
}
} else if (sgpdVersion >= 2) {
long default_length = sgpdVersion >= 1 ? sgpd.readUnsignedInt() : 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think the >= 1 check here is correct - I think it should be == 1.

Looking at section 8.9.3.2 of ISO 14496-12:2015 it seems default_length is present at v1 only, and the same 4 bytes are default_sample_description_index on v2+:

if (version==1) { unsigned int(32) default_length; }
if (version>=2) {
  unsigned int(32) default_sample_description_index;
}

This also matches the previous code.

Same below on the condition gating the description_length skip.

@dimitry-unified-streaming dimitry-unified-streaming Jun 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the 2022 and 2024 DIS versions of ISO 14496-12 here, and those both have the following:

aligned(8) class SampleGroupDescriptionBox ()
  extends FullBox('sgpd', version, flags){
  unsigned int(32) grouping_type;
  if (version>=1) { unsigned int(32) default_length; }
  if (version>=2) {
    unsigned int(32) default_group_description_index;
  }
  unsigned int(32) entry_count;
  int i;
  for (i = 1 ; i <= entry_count ; i++){
    if (version>=1) {
     if (default_length==0) {
       unsigned int(32) description_length;
      }
    }
    SampleGroupDescriptionEntry (grouping_type);
    // an instance of a class derived from SampleGroupDescriptionEntry
    // that is appropriate and permitted for the media type
  }
}

I.e. default_length exists for version>=1, and default_group_description_index exists for version>=2. This is also how we write sgpd boxes in our own software.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, thanks - aren't these two versions of the spec binary-incompatible with each other? My understanding was that this was generally avoided in these sort of specs. Was this an (unacknowledged?) bug in the 2012 to 2020 versions of the spec?

Given this is quite a fiddly change, and there's a risk that it's quite hard to find this PR comment thread in future when someone is going through the blame layer, I'm going to send a change that pretty much only changes == 1 to >= 1 with an explanation - and then we can rebase this PR on top - hope that's OK.

}

@Test
public void extract_h264WithVariableLengthSgpdBox() throws Exception {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test fail for you without the fix? It seems to pass for me

To show this, I checked out this PR, then ran:

$ git checkout HEAD^ -- libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java

And then ran the tests in this file, and they all passed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I see what is happening; the parseSampleGroups() function scans only for seig grouping types, not roll ones. When it doesn't find any seig grouping type, it does an early return, and doesn't hit the sgpd box parsing code at all.

I had troubles getting any test to work with seig boxes, which is why the sample_fragmented_variable_length_sgpd.mp4 file has another grouping type roll, e.g. MP4Box output:

<SampleGroupBox Size="28" Type="sbgp" Version="0" Flags="0" Specification="p12" Container="stbl traf" grouping_type="roll">
  <SampleGroupBoxEntry sample_count="94" group_description_index="1" group_description_in_traf="1"/>
</SampleGroupBox>
<SampleGroupDescriptionBox Size="30" Type="sgpd" Version="1" Flags="0" Specification="p12" Container="stbl traf" grouping_type="roll" default_length="0">
  <RollRecoveryEntry roll_distance="-1"/>
</SampleGroupDescriptionBox>

I'll try this again with a file with a seig box, but previously I got a deduplicateConsecutiveFormats=false so TrackOutput must receive at least one sampleMetadata() call between format() calls. error on that. I had no idea what to do to work around that other error. Maybe I have to supply some DRM metadata somewhere to make such a test work?

@icbaker

icbaker commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

I'll try this again with a file with a seig box, but previously I got a deduplicateConsecutiveFormats=false so TrackOutput must receive at least one sampleMetadata() call between format() calls. error on that. I had no idea what to do to work around that other error.

If you get stuck on this again, please can you add the file that causes the error to the PR so I can have a play with it too?

@dimitry-unified-streaming dimitry-unified-streaming force-pushed the fix-variable-length-sgpd-main-1 branch from faa3e25 to 6a8d81f Compare June 5, 2026 11:09
@dimitry-unified-streaming

Copy link
Copy Markdown
Author

I replaced the sample_fragmented_variable_length_sgpd.mp4 file with a file with an actual seig box. If you check this with e.g. MP4Box -diso, you will see:

<SampleGroupBox Size="28" Type="sbgp" Version="0" Flags="0" Specification="p12" Container="stbl traf" grouping_type="seig">
  <SampleGroupBoxEntry sample_count="48" group_description_index="1" group_description_in_traf="1"/>
</SampleGroupBox>
<SampleGroupDescriptionBox Size="65" Type="sgpd" Version="1" Flags="0" Specification="p12" Container="stbl traf" grouping_type="seig" default_length="0">
  <CENCSampleEncryptionGroupEntry IsEncrypted="1" IV_size="0" KID="0x804EA17317113C8B955B35996E5E3D92" constant_IV_size="16" constant_IV="0xDCC29B585406F0F0031C7D01C020C321"/>
</SampleGroupDescriptionBox>

However, as I indicated, this file leads to a different exception, which I do not know how to solve or work around:


deduplicateConsecutiveFormats=false so TrackOutput must receive at least one sampleMetadata() call between format() calls.
java.lang.IllegalStateException: deduplicateConsecutiveFormats=false so TrackOutput must receive at least one sampleMetadata() call between format() calls.
	at com.google.common.base.Preconditions.checkState(Preconditions.java:513)
	at androidx.media3.test.utils.FakeTrackOutput.format(FakeTrackOutput.java:90)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor$TrackBundle.updateDrmInitData(FragmentedMp4Extractor.java:2372)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor.onMoofContainerAtomRead(FragmentedMp4Extractor.java:920)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor.onContainerAtomRead(FragmentedMp4Extractor.java:789)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor.processAtomEnded(FragmentedMp4Extractor.java:758)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor.readAtomPayload(FragmentedMp4Extractor.java:753)
	at androidx.media3.extractor.mp4.FragmentedMp4Extractor.read(FragmentedMp4Extractor.java:586)
	at androidx.media3.test.utils.TestUtil.extractAllSamplesFromByteArray(TestUtil.java:614)
	at androidx.media3.test.utils.TestUtil.extractAllSamplesFromFile(TestUtil.java:577)
	at androidx.media3.extractor.mp4.FragmentedMp4ExtractorNonParameterizedTest.extract_h264WithVariableLengthSgpdBox(FragmentedMp4ExtractorNonParameterizedTest.java:134)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.base/java.lang.Thread.run(Unknown Source)

@icbaker

icbaker commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks - I had a play with the file you provided and concluded that the duplicate format is expected because the sgpd is coming from within the moof and not the moov - which triggers a second format to be emitted when it's found. I've updated the test to allow this duplicate format for this specific sample file.

copybara-service Bot pushed a commit that referenced this pull request Jun 10, 2026
In the 2012 to 2020 versions of the spec, the binary format was
documented as:

```
if (version==1) { unsigned int(32) default_length; }
if (version>=2) {
  unsigned int(32) default_sample_description_index;
}
```

While in the 2022 spec this has changed (in a binary incompatible
way!) to:

```
if (version>=1) { unsigned int(32) default_length; }
if (version>=2) {
  unsigned int(32) default_group_description_index;
}
```

The later specs take precedence, so this change updates our parsing
code to follow the 2022 version.

This is a precursor to Issue: #3243 (where the mismatch was
[discovered/discussed](#3243 (comment))).

PiperOrigin-RevId: 929815518
For `sgpd` boxes with version >= 1, `the default_length` field is zero if the
length of the following sample group entries is variable.

Since the `FragmentedMp4Extractor` only supports one entry, skip the
`description_length` field if applicable (e.g. when version >= 1 and
`default_length` == 0), instead of throwing an error.

For parsing the following CencSampleEncryptionInformationGroupEntry, this should
make no difference.

Issue: androidx#3177
@icbaker icbaker force-pushed the fix-variable-length-sgpd-main-1 branch 3 times, most recently from 1999196 to 5f6c076 Compare June 10, 2026 15:11
…pdate dump files

Also add a release note and fix a local variable name
@icbaker icbaker force-pushed the fix-variable-length-sgpd-main-1 branch from 5f6c076 to fdcea3a Compare June 10, 2026 15:19
@icbaker

icbaker commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

I'm going to send this for internal review now. You may see some more commits being added as I make changes in response to review feedback. Please refrain from pushing any more substantive changes as it will complicate the internal review - thanks!

@dimitry-unified-streaming

Copy link
Copy Markdown
Author

Thanks for the updates and rebasing. I have tested your updates here locally, and they seem to all be successful.

@copybara-service copybara-service Bot merged commit 07c1dfb into androidx:main Jun 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants