vb.neth.264

H.264 SPS parsing for frame dimension


I am currently working on an H.264 decoder. I have already studied the documentation for MP4 and NALUs, and I have a NALU list as well as the so-called DecoderConfigurationRecord, from which I read the Sequence Parameter Set. I am having issues with this SPS.

From my previous question, I know that everything up to the variable max_frame_num is correct—both the bit positions and the corresponding values in the variables. Now, I've done some additional programming and noticed that the values for pic_width_in_mbs_minus1 and pic_height_in_map_units_minus1 are incorrect for all videos. These are videos from several recorders with different dimensions. I assume my DecodeExpGolomb function is correct. The new function I've been using since last time is the DecodeExpGolombSigned function, and I suspect the error lies there. In one video, the bit position is progressing too far, causing the byte array of the SPS to be exceeded.

I've created a test project for you (WinForms application)—as small as possible but containing everything necessary. The reading of the NALU header/extension is not included here. By the way, this time I've added all the bytes of the SPS.

The values I expect are 1800px for the width and 2700px for the height. Because the video in this example only has 3 frames, I also expect a plausible value for max_num_ref_frames.
I would appreciate any help, as I find these byte functions a bit challenging.

Bitstream
Friend NotInheritable Class BitStream
    Friend Property BytePos As Integer
        Get
            Return _BytePos
        End Get
        Private Set
            _BytePos = Value
            UpdateAbsBitPos()
        End Set
    End Property

    Friend Property BitPos As Integer
        Get
            Return _BitPos
        End Get
        Private Set
            _BitPos = Value
            UpdateAbsBitPos()
        End Set
    End Property

    ''' <summary>
    ''' as index 0–7
    ''' </summary>
    ''' <returns></returns>
    Friend Property AbsBitPos As Integer
        Get
            Return _AbsBitPos
        End Get
        Private Set
            _AbsBitPos = Value
        End Set
    End Property

    Private ReadOnly _paddingLength As Integer
    Private _BytePos As Integer = 0
    Private _BitPos As Integer = 0
    Private _AbsBitPos As Integer

    Friend Sub New(paddingLength As Integer)
        Me._paddingLength = paddingLength
        UpdateAbsBitPos()
    End Sub

    Friend Sub SetInitialPositions(byteNumber As Integer, bitNumber As Integer)
        Me.BytePos = byteNumber
        Me.BitPos = bitNumber
    End Sub

    Friend Sub SetBitPosition(bitPosition As Integer)
        Me.BytePos = bitPosition \ 8
        Me.BitPos = bitPosition Mod 8
    End Sub

    Friend Sub IncrementBytePosition(byteNumber As Integer)
        Dim totalBits As Integer = (Me.BytePos + byteNumber) * 8 + Me.BitPos
        SetBitPosition(totalBits)
    End Sub

    Friend Sub IncrementBitPosition(bitNumber As Integer)
        Dim totalBits As Integer = (Me.BytePos * 8) + Me.BitPos + bitNumber
        SetBitPosition(totalBits)
    End Sub

    Public Overrides Function ToString() As String
        Return $"Byte: {Me.BytePos.ToString(New Globalization.CultureInfo("en-GB")).PadLeft(_paddingLength, " "c)}, Bit: {Me.BitPos}, Absolute BitPos: {Me.AbsBitPos}"
    End Function

    ''' <summary>
    ''' Updates the current bit position to a new absolute bit position.
    ''' Automatically adjusts the byte position as necessary.
    ''' </summary>
    ''' <param name="newBitPos"></param>
    Friend Sub UpdateBitPosition(newBitPos As Integer)
        SetBitPosition(newBitPos)
    End Sub

    ''' <summary>
    ''' Updates the current bit position to a new absolute bit position.
    ''' Automatically adjusts the byte position as necessary.
    ''' </summary>
    ''' <param name="newBitPos"></param>
    Friend Sub UpdateBitPosition(newBitPos As UInteger)
        SetBitPosition(CInt(newBitPos))
    End Sub

    ''' <summary>
    ''' Updates the absolute bit position based on the current byte and bit positions.
    ''' </summary>
    Private Sub UpdateAbsBitPos()
        Me.AbsBitPos = (Me.BytePos * 8) + Me.BitPos
    End Sub
End Class
SequenceParameterSet
Friend NotInheritable Class SequenceParameterSet
    Friend ReadOnly Property SequenceParameterSetLength As UInt16
    Friend ReadOnly Property SequenceParameterSetNALUnit As Byte()

    Friend Sub New(sequenceParameterSetLength As UInt16, sequenceParameterSetNALUnit As Byte())
        Me.SequenceParameterSetLength = sequenceParameterSetLength
        Me.SequenceParameterSetNALUnit = sequenceParameterSetNALUnit
    End Sub
End Class
ByteTools
Friend NotInheritable Class ByteTools
    Friend Shared Function ReadBitsWithinOneByte(value As Byte, startPositionIndexFrom As Integer, numBitsToRead As Integer) As Byte
        Return value << startPositionIndexFrom >> (8 - numBitsToRead) << (8 - numBitsToRead - startPositionIndexFrom)
    End Function

    Friend Shared Function DecodeExpGolomb(byteStream As Byte(), ByRef nextPos As UInteger) As UInt32
        'https://stackoverflow.com/a/39841215/13936657
        If byteStream Is Nothing Then
            Throw New ArgumentException("The byte array must not be null.", NameOf(byteStream))
        ElseIf nextPos = 0UI Then
            Throw New ArgumentException("Index must not be 0.", NameOf(nextPos))
        End If

        Dim leadingZeroBits As UInteger = UInt32.MaxValue

        Dim pos As UInteger = nextPos

        Dim b As Integer = 0
        While b = 0

            b = GetBitByPosArray(byteStream, CInt(pos))
            pos += 1UI
            If leadingZeroBits < UInt32.MaxValue Then ' In the original C++ code, leadingZeroBits is an uint32 (as here) but is initialized with -1 (so, UInt32.MaxValue), and overflows without exception.
                leadingZeroBits += 1UI
            Else
                leadingZeroBits = 0UI
            End If
        End While

        ' In the original C++ code, there was a for loop with bit-shifting on this line, but it was incorrect. In fact, that answer was intended to correct a previous one.

        nextPos = pos
        Dim bitCount As Integer = Math.Max(1, CInt(leadingZeroBits)) ' Always read at least 1 bit
        Dim readBitsResult As UInteger = ReadBits(byteStream, CInt(pos) - 1, bitCount) ' Starting from the position with the "1" where the while loop stops counting the leading zeros
        If leadingZeroBits > 0UI Then
            nextPos += CUInt(bitCount)
        End If
        Dim extractedValue As UInteger = CUInt(Math.Pow(2, leadingZeroBits)) - 1UI + readBitsResult
        Return extractedValue
    End Function

    Friend Shared Function DecodeExpGolombSigned(byteStream() As Byte, ByRef nextPos As UInteger) As Integer
        If byteStream Is Nothing Then
            Throw New ArgumentException("The byte array must not be null.", NameOf(byteStream))
        ElseIf nextPos = 0UI Then
            Throw New ArgumentException("Index must not be 0.", NameOf(nextPos))
        End If

        Dim leadingZeroBits As UInteger = UInt32.MaxValue

        Dim pos As UInteger = nextPos

        Dim b As Integer = 0
        While b = 0

            b = GetBitByPosArray(byteStream, CInt(pos))
            pos += 1UI
            If leadingZeroBits < UInt32.MaxValue Then ' In the original C++ code, leadingZeroBits is an uint32 (as here) but is initialized with -1 (so, UInt32.MaxValue), and overflows without exception.
                leadingZeroBits += 1UI
            Else
                leadingZeroBits = 0UI
            End If
        End While

        ' In the original C++ code, there was a for loop with bit-shifting on this line, but it was incorrect. In fact, that answer was intended to correct a previous one.

        nextPos = pos
        Dim bitCount As Integer = Math.Max(1, CInt(leadingZeroBits)) ' Always read at least 1 bit
        Dim tmpResult As Integer = CInt(ReadBits(byteStream, CInt(pos) - 1, bitCount)) ' Starting from the position with the "1" where the while loop stops counting the leading zeros
        If leadingZeroBits > 0UI Then
            nextPos += CUInt(bitCount)
        End If
        Dim result As Integer
        If (tmpResult And 1) > 0 Then
            result = (tmpResult + 1) >> 1
        Else
            result = -(tmpResult >> 1)
        End If
        Return result
    End Function

    Friend Shared Function ReadBits(bytes As Byte(), bitStartPositionAsIndex As Integer, bitCount As Integer) As UInteger
        If bitCount = 0 Then
            Throw New ArgumentException($"{NameOf(bitCount)} must not be 0.")
        End If
        If bytes Is Nothing Then
            Throw New ArgumentException("The byte array must not be null.", NameOf(bytes))
        End If
        If bitStartPositionAsIndex >= bytes.Length * 8 Then
            Throw New ArgumentException($"{NameOf(bitStartPositionAsIndex)} is too large ({bitStartPositionAsIndex}) for the byte array with a size of {bytes.Length} bytes.")
        End If
        If bitCount > 32 Then
            Throw New ArgumentException("UInt32 can only hold up to 32 bits.")
        End If

        Dim startIndex As Integer = bitStartPositionAsIndex \ 8
        Dim endIndex As Integer = (bitStartPositionAsIndex + bitCount - 1) \ 8
        Dim remE As Integer = (bitStartPositionAsIndex + bitCount) Mod 8
        Dim a As UInt32 = bytes.Skip(startIndex).Take(1 + endIndex - startIndex).Select(AddressOf Convert.ToUInt32).Aggregate(Function(x, y) x << 8 Or y)
        Return a >> ((8 - remE) Mod 8) And (1UI << bitCount) - 1UI
    End Function

    Private Shared Function GetBitByPosArray(buffer As Byte(), pos As Integer) As Byte
        If buffer Is Nothing Then
            Throw New ArgumentException("The byte array must not be null.", NameOf(buffer))
        End If

        pos = pos Mod (buffer.Length * 8)
        Return CByte((buffer(pos \ 8) >> (7 - pos Mod 8)) And 1)
    End Function

    ''' <summary>
    ''' Retrieves a single bit (0 or 1) from a given byte at a specified position, and returns a boolean.
    ''' </summary>
    ''' <param name="value"></param>
    ''' <param name="index"></param>
    ''' <returns></returns>
    Friend Shared Function GetBitB(value As Byte, index As Integer) As Boolean
        If index < 0 Then
            Throw New ArgumentException($"{NameOf(index)} cannot be less than 0.", NameOf(index))
        ElseIf index >= 8 Then
            Throw New ArgumentException($"{NameOf(index)} cannot be greater than or equal to 8.", NameOf(index))
        End If

        index = index Mod 8
        Return CByte((value >> (7 - index)) And 1) > 0
    End Function

   Friend Shared Function GetBit(value As Byte, index As Integer) As Byte
        If index < 0 Then
            Throw New ArgumentException($"{NameOf(index)} cannot be less than 0.", NameOf(index))
        ElseIf index >= 8 Then
            Throw New ArgumentException($"{NameOf(index)} cannot be greater than or equal to 8.", NameOf(index))
        End If

        index = index Mod 8
        Return CByte((value >> (7 - index)) And 1)
    End Function
End Class
Reading SPS
Friend NotInheritable Class Form1
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' Some things here, e.g. NALU header
        '...

        Dim spsList As New List(Of SequenceParameterSet)()
        Dim spsTest As New SequenceParameterSet(27US, New Byte() {103, 100, 0, 51, 172, 217, 0, 113, 1, 83, 229, 188, 4, 64, 0, 0, 3, 0, 64, 0, 0, 15, 3, 198, 12, 101, 128})
        spsList.Add(spsTest)
        ReadSps(spsList, 3UI)
    End Sub

    ''' <summary>
    ''' The SPS contains a special profile (HDR, etc) und thus more fields.
    ''' </summary>
    ''' <param name="id"></param>
    ''' <returns></returns>
    Private Shared Function IsSpecialSpsProfile(id As UInt32) As Boolean
        Dim ids As New List(Of UInt32) From {100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135}
        Return ids.Contains(id)
    End Function

    Private Sub ScalingList(ByRef scalingList As Integer(), sizeOfScalingList As Integer, ByRef useDefaultScalingMatrixFlag As Boolean, byteStream As Byte(), ByRef nextPos As UInteger)
        Dim lastScale As Integer = 8
        Dim nextScale As Integer = 8
        Dim delta_scale As Integer

        For j As Integer = 0 To sizeOfScalingList - 1
            If nextScale <> 0 Then
                delta_scale = ByteTools.DecodeExpGolombSigned(byteStream, nextPos)
                nextScale = (lastScale + delta_scale + 256) Mod 256
                useDefaultScalingMatrixFlag = (j = 0 AndAlso nextScale = 0)
            End If

            scalingList(j) = If(nextScale = 0, lastScale, nextScale)
            lastScale = scalingList(j)
        Next
    End Sub

   Private Sub ReadSps(spsList As List(Of SequenceParameterSet), numberOfFramesInTrack As UInteger)

        Dim bs As New BitStream(7)

        ' ===================
        '  Start of SPS data
        ' ===================
        Dim hasFinishedSps As Boolean = False
        If spsList.Count > 0 AndAlso Not hasFinishedSps Then ' SPS from DecoderConfigurationRecord → SPS List
            Dim sps As SequenceParameterSet = spsList.First()
            Dim profile_idc As Byte = sps.SequenceParameterSetNALUnit(1)
            Dim constraint_set0_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 0)
            Dim constraint_set1_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 1)
            Dim constraint_set2_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 2)
            Dim constraint_set3_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 3)
            Dim constraint_set4_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 4)
            Dim constraint_set5_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(2), 5)
            Dim reserved_zero_2bits As Byte = ByteTools.ReadBitsWithinOneByte(sps.SequenceParameterSetNALUnit(2), 6, 2)
#If DEBUG Then
            If reserved_zero_2bits <> CByte(0) Then
                System.Diagnostics.Debug.WriteLine($"{NameOf(reserved_zero_2bits)}: {reserved_zero_2bits}. Supposed to be 0, but has to be ignored anyway.")
            End If
#End If
            Dim level_idc As Byte = sps.SequenceParameterSetNALUnit(3)

            bs.SetInitialPositions(4, 0) '5. Byte
            Dim absoluteBitPos As UInteger = CUInt(bs.AbsBitPos)

            '0–31
            Dim seq_Parameter_set_id As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)
#If DEBUG Then
            If seq_Parameter_set_id > 31UI Then
                System.Diagnostics.Debug.WriteLine($"{NameOf(seq_Parameter_set_id)}: {seq_Parameter_set_id}")
            End If
#End If

            Dim ChromaArrayType As Byte = 0
            Dim SubWidthC As UInt32? = Nothing
            Dim SubHeightC As UInt32? = Nothing
            If IsSpecialSpsProfile(profile_idc) Then

                '0–3
                Dim chroma_format_idc As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
#If DEBUG Then
                If chroma_format_idc > 3UI Then
                    System.Diagnostics.Debug.WriteLine($"{NameOf(chroma_format_idc)} should not be greater than 3, but is: {chroma_format_idc}.")
                End If
#End If

                Dim separate_colour_plane_flag As Byte = 0
                If chroma_format_idc = CByte(3) Then
                    separate_colour_plane_flag = ByteTools.GetBit(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                    bs.IncrementBitPosition(1)
                End If

                If chroma_format_idc = CByte(1) OrElse chroma_format_idc = CByte(2) Then
                    SubWidthC = 2UI
                    SubHeightC = 2UI
                ElseIf chroma_format_idc = CByte(3) AndAlso separate_colour_plane_flag = CByte(0) Then
                    SubWidthC = 1UI
                    SubHeightC = 1UI
                End If

                If separate_colour_plane_flag = CByte(0) Then
                    ChromaArrayType = CByte(chroma_format_idc)
                Else
                    ChromaArrayType = 0
                End If

                '0–6
                Dim bit_depth_luma_minus8 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
#If DEBUG Then
                If bit_depth_luma_minus8 > 6UI Then
                    System.Diagnostics.Debug.WriteLine($"{NameOf(bit_depth_luma_minus8)}: {bit_depth_luma_minus8}")
                End If
#End If

                ' Bit depth 0–6
                Dim bit_depth_chroma_minus8 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
#If DEBUG Then
                If bit_depth_chroma_minus8 > 6 Then
                    System.Diagnostics.Debug.WriteLine($"{NameOf(bit_depth_chroma_minus8)}: {bit_depth_chroma_minus8 }")
                End If
#End If
                Dim qpprime_y_zero_transform_bypass_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                bs.IncrementBitPosition(1)

                Dim seq_scaling_matrix_present_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                bs.IncrementBitPosition(1)
                Dim Flat_4x4_16 As Integer() = Enumerable.Repeat(16, 16).ToArray() ' 4×4 Matrix, each value is 16
                Dim Flat_8x8_16 As Integer() = Enumerable.Repeat(16, 64).ToArray() ' 8×8 Matrix, each value is 16
                Dim limit As Integer = If(chroma_format_idc <> 3, 8, 12)
                Dim ScalingList4x4 As Integer() = New Integer(15) {}
                Dim ScalingList8x8 As Integer() = New Integer(63) {}
                Dim UseDefaultScalingMatrix4x4Flag As Boolean = False
                Dim UseDefaultScalingMatrix8x8Flag As Boolean = False
                If seq_scaling_matrix_present_flag Then
                    Dim seq_scaling_list_present_flag As Boolean() = New Boolean(limit - 1) {}
                    For i As Integer = 0 To limit - 1
                        seq_scaling_list_present_flag(i) = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                        bs.IncrementBitPosition(1)
                        If seq_scaling_list_present_flag(i) Then
                            absoluteBitPos = CUInt(bs.AbsBitPos)
                            If i < 6 Then
                                ' If seq_scaling_list_present_flag(i) is set, use ScalingList4x4
                                ScalingList(ScalingList4x4, 16, UseDefaultScalingMatrix4x4Flag, sps.SequenceParameterSetNALUnit, absoluteBitPos)
                            Else
                                ' Use ScalingList8x8 if i >= 6
                                ScalingList(ScalingList8x8, 64, UseDefaultScalingMatrix8x8Flag, sps.SequenceParameterSetNALUnit, absoluteBitPos)
                            End If
                            bs.UpdateBitPosition(absoluteBitPos)
                        Else
                            ' If seq_scaling_list_present_flag(i) is not set, use the default matrices
                            If i < 6 Then
                                ScalingList4x4(i) = Flat_4x4_16(i) ' Set the default 4×4 matrix for i = 0..5
                            Else
                                ScalingList8x8(i - 6) = Flat_8x8_16(i - 6) ' Set the default 8×8-Matrix for i = 6..11
                            End If
                        End If
                    Next
                Else
                    ' If seq_scaling_matrix_present_flag = 0, use only the default matrices
                    For i As Integer = 0 To 5
                        ScalingList4x4(i) = Flat_4x4_16(i)
                    Next
                    For i As Integer = 6 To 11
                        ScalingList8x8(i - 6) = Flat_8x8_16(i - 6)
                    Next
                End If
            End If

            ' – – – – – – – –
            absoluteBitPos = CUInt(bs.AbsBitPos)
            ' supposed to equal to 0–12
            Dim log2_max_frame_num_minus4 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)
#If DEBUG Then
            If log2_max_frame_num_minus4 > 12UI Then
                System.Diagnostics.Debug.WriteLine($"{NameOf(log2_max_frame_num_minus4)} is greater than 12: {String.Format("{0:n0}", log2_max_frame_num_minus4)}")
            End If
#End If
            Dim max_frame_num As UInt32 = CUInt(Math.Pow(2, log2_max_frame_num_minus4 + 4))
            Dim fallback_max_frame_num As UInt32 = CUInt(Math.Pow(2, Math.Ceiling(Math.Log(numberOfFramesInTrack) / Math.Log(2))))
            ' Validate max_frame_num
            If max_frame_num < numberOfFramesInTrack OrElse max_frame_num > fallback_max_frame_num * 2 Then
                ' Fallback if max_frame_num is unreasonably low or too high
                'System.Diagnostics.Debug.WriteLine($"Fallback triggered: Parsed max_frame_num = {max_frame_num}, Fallback (german numbering) = {String.Format(New System.Globalization.CultureInfo("de-DE"), "{0:n0}", fallback_max_frame_num)}")
                max_frame_num = fallback_max_frame_num
            End If
            ' – – – – – – – –
            ' 0–2
            Dim pic_order_cnt_type As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)

            If pic_order_cnt_type = 0UI Then
                ' 0–12
                Dim log2_max_pic_order_cnt_lsb_minus4 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
                Dim MaxPicOrderCntLsb As UInt32 = CUInt(Math.Pow(2, log2_max_pic_order_cnt_lsb_minus4 + 4))

            ElseIf pic_order_cnt_type = 1UI Then
                Dim delta_pic_order_always_zero_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                bs.IncrementBitPosition(1)
                absoluteBitPos = CUInt(bs.AbsBitPos)

                ' -(2^31) to (2^31)-1
                Dim offset_for_non_ref_pic As Integer = ByteTools.DecodeExpGolombSigned(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)

                ' -(2^31) to (2^31)-1
                Dim offset_for_top_to_bottom_field As Integer = ByteTools.DecodeExpGolombSigned(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)

                ' 0–255
                Dim num_ref_frames_in_pic_order_cnt_cycle As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
                Dim offsets_for_ref_frame As New List(Of Integer)()
                For i As Integer = 0 To CInt(num_ref_frames_in_pic_order_cnt_cycle) - 1 Step 1
                    Dim offset As Integer = ByteTools.DecodeExpGolombSigned(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                    offsets_for_ref_frame.Add(offset)
                    bs.UpdateBitPosition(absoluteBitPos)
                Next
            End If

            Dim max_num_ref_frames As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)

            Dim gaps_in_frame_num_value_allowed_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
            bs.IncrementBitPosition(1)
            absoluteBitPos = CUInt(bs.AbsBitPos)

            ' in Macro blocks (16×16 Pixel)
            Dim pic_width_in_mbs_minus1 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)

            Dim pic_height_in_map_units_minus1 As UInt32 = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
            bs.UpdateBitPosition(absoluteBitPos)

            Dim frame_mbs_only_flag As Byte = ByteTools.GetBit(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
            bs.IncrementBitPosition(1)
            absoluteBitPos = CUInt(bs.AbsBitPos)

            If frame_mbs_only_flag = CByte(0) Then
                Dim mb_adaptive_frame_field_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
                bs.IncrementBitPosition(1)
                absoluteBitPos = CUInt(bs.AbsBitPos)
            End If

            Dim direct_8x8_inference_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
            bs.IncrementBitPosition(1)
            absoluteBitPos = CUInt(bs.AbsBitPos)

            Dim frame_cropping_flag As Boolean = ByteTools.GetBitB(sps.SequenceParameterSetNALUnit(bs.BytePos), bs.BitPos)
            bs.IncrementBitPosition(1)
            absoluteBitPos = CUInt(bs.AbsBitPos)

            Dim frame_crop_left_offset As UInt32 = 0UI
            Dim frame_crop_right_offset As UInt32 = 0UI
            Dim frame_crop_top_offset As UInt32 = 0UI
            Dim frame_crop_bottom_offset As UInt32 = 0UI
            If frame_cropping_flag Then
                frame_crop_left_offset = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
                frame_crop_right_offset = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
                frame_crop_top_offset = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
                frame_crop_bottom_offset = ByteTools.DecodeExpGolomb(sps.SequenceParameterSetNALUnit, absoluteBitPos)
                bs.UpdateBitPosition(absoluteBitPos)
            End If

            Dim CropUnitX As UInt32 = 0UI
            Dim CropUnitY As UInt32 = 0UI
            Dim PicWidthInSamples As UInt32 = (pic_width_in_mbs_minus1 + 1UI) * 16UI
            Dim PicHeightInMapUnits As UInt32 = pic_height_in_map_units_minus1 + 1UI
            Dim FrameHeightInMbs As UInt32 = (2UI - frame_mbs_only_flag) * PicHeightInMapUnits
            If ChromaArrayType = CByte(0) Then
                CropUnitX = 1UI
                CropUnitY = 2UI - frame_mbs_only_flag
            Else
                If SubWidthC.HasValue Then
                    CropUnitX = SubWidthC.Value
                End If
                If SubHeightC.HasValue Then
                    CropUnitY = SubHeightC.Value * (2UI - frame_mbs_only_flag)
                End If
            End If
            Dim finalWidth As UInt32 = PicWidthInSamples - CropUnitX * (frame_crop_left_offset + frame_crop_right_offset)
            System.Diagnostics.Debug.WriteLine($"Width: {finalWidth} px")
            Dim finalHeight As UInt32 = 16UI * FrameHeightInMbs - CropUnitY * (frame_crop_top_offset + frame_crop_bottom_offset)


            hasFinishedSps = True
            'ElseIf currentNalu.Type = CByte(7) Then ' SPS from Nalu

        End If
    End Sub
End Class

Solution

  • "I noticed that the values for pic_width_in_mbs_minus1 and pic_height_in_map_units_minus1 are incorrect for all videos."

    "The values I expect are 1800px for the width and 2700px for the height. Because the video in this example only has 3 frames, I also expect a plausible value for max_num_ref_frames."

    If you are getting 112 for pic_width_in_mbs_minus1 then your code is correct.

    An MB (or MacroBlock) is a rectangular block of pixels, width is 16px and height is 16px.

    112 * mb_width_16px == (112 * 16) == 1792px width //# 112 is mbs_minus1
    113 * mb_width_16px == (113 * 16) == 1808px width //# 113 is mbs_minus1 +1
    

    Your picture width is 1800px so it needs 113 MBs. The minus1, as encoded in SPS, is 112.

    max_num_ref_frames is for configuring the H264 decoder. It has nothing to do with any total video frame count. Even a 1 frame video can have a reference frame count of 4.

    Your max_num_ref_frames seems to be encoded as 3. Do you get that number from your code?

    "I would appreciate any help, as I find these byte functions a bit challenging."

    One way for doing some faster double-checking is to use an H264 analysis tool.
    Such tools will show you the values of the existing H264 variables according to your NALU's bytes structure then now it's faster to confirm if the code is reading correctly (or if not, then adjust code to match results).

    I like CodecVisa (with 30-day free testing) but you can get the same NALU header results (minus the beautiful GUI) for free using FFmpeg. I suspect you already have an FFmpeg executable (runs in commandline)?

    Did you know FFmpeg can print details about a NALU's header variables?

    ffmpeg -i test.h264 -c copy -bsf:v trace_headers -f null - 2> nalu_results.txt
    

    example:

    Run this FFmpeg command:

    ffmpeg -i testnalu.h264 -c copy -bsf:v trace_headers -f null - 2> nalu_result_sps.txt
    

    Then the print out looks like:

    where layout is: [ bit-pos, H264 variable name, bits of variable, =, decimal value]
    
     0           forbidden_zero_bit                                          0 = 0
     1           nal_ref_idc                                                11 = 3
     3           nal_unit_type                                           00111 = 7
     8           profile_idc                                          01100100 = 100
     16          constraint_set0_flag                                        0 = 0
     17          constraint_set1_flag                                        0 = 0
     18          constraint_set2_flag                                        0 = 0
     19          constraint_set3_flag                                        0 = 0
     20          constraint_set4_flag                                        0 = 0
     21          constraint_set5_flag                                        0 = 0
     22          reserved_zero_2bits                                        00 = 0
     24          level_idc                                            00110011 = 51
     32          seq_parameter_set_id                                        1 = 0
     33          chroma_format_idc                                         010 = 1
     36          bit_depth_luma_minus8                                       1 = 0
     37          bit_depth_chroma_minus8                                     1 = 0
     38          qpprime_y_zero_transform_bypass_flag                        0 = 0
     39          seq_scaling_matrix_present_flag                             0 = 0
     40          log2_max_frame_num_minus4                                   1 = 0
     41          pic_order_cnt_type                                          1 = 0
     42          log2_max_pic_order_cnt_lsb_minus4                         011 = 2
     45          max_num_ref_frames                                      00100 = 3
     50          gaps_in_frame_num_allowed_flag                              0 = 0
     51          pic_width_in_mbs_minus1                         0000001110001 = 112
     64          pic_height_in_map_units_minus1                000000010101001 = 168
     79          frame_mbs_only_flag                                         1 = 1
     80          direct_8x8_inference_flag                                   1 = 1
     81          frame_cropping_flag                                         1 = 1
     82          frame_crop_left_offset                                      1 = 0
     83          frame_crop_right_offset                                 00101 = 4
     88          frame_crop_top_offset                                       1 = 0
     89          frame_crop_bottom_offset                                  011 = 2
     92          vui_parameters_present_flag                                 1 = 1
     93          aspect_ratio_info_present_flag                              1 = 1
     94          aspect_ratio_idc                                     00000001 = 1
     102         overscan_info_present_flag                                  0 = 0
     103         video_signal_type_present_flag                              0 = 0
     104         chroma_loc_info_present_flag                                0 = 0
     105         timing_info_present_flag                                    1 = 1
     106         num_units_in_tick            00000000000000000000000000000001 = 1
     138         time_scale                   00000000000000000000000000111100 = 60
     170         fixed_frame_rate_flag                                       0 = 0
     171         nal_hrd_parameters_present_flag                             0 = 0
     172         vcl_hrd_parameters_present_flag                             0 = 0
     173         pic_struct_present_flag                                     0 = 0
     174         bitstream_restriction_flag                                  1 = 1
     175         motion_vectors_over_pic_boundaries_flag                     1 = 1
     176         max_bytes_per_pic_denom                                     1 = 0
     177         max_bits_per_mb_denom                                       1 = 0
     178         log2_max_mv_length_horizontal                         0001100 = 11
     185         log2_max_mv_length_vertical                           0001100 = 11
     192         max_num_reorder_frames                                    011 = 2
     195         max_dec_frame_buffering                                 00101 = 4
     200         rbsp_stop_one_bit                                           1 = 1
     201         rbsp_alignment_zero_bit                                     0 = 0
     202         rbsp_alignment_zero_bit                                     0 = 0
     203         rbsp_alignment_zero_bit                                     0 = 0
     204         rbsp_alignment_zero_bit                                     0 = 0
     205         rbsp_alignment_zero_bit                                     0 = 0
     206         rbsp_alignment_zero_bit                                     0 = 0
     207         rbsp_alignment_zero_bit                                     0 = 0