Public/Build-SampleMkvFromSrs.ps1

function Build-SampleMkvFromSrs {
    <#
    .SYNOPSIS
        Reconstruct sample MKV by hierarchically parsing SRS and injecting track data.

    .DESCRIPTION
        Hierarchical EBML parsing approach:
        1. Read SRS file element-by-element using proper EBML VLQ decoding
        2. For container elements (Segment, Cluster, BlockGroup): write header, descend into children
        3. Skip ReSample container entirely (metadata only, not in final output)
        4. For Block/SimpleBlock elements: write header + block header + injected track data
        5. For all other elements: copy header + content directly

    .PARAMETER SrsFilePath
        Path to the SRS file containing sample structure.

    .PARAMETER TrackDataFiles
        Hashtable mapping track numbers to extracted track data file paths.

    .PARAMETER OutputMkvPath
        Path for the output reconstructed MKV file.

    .EXAMPLE
        Build-SampleMkvFromSrs -SrsFilePath "sample.srs" -TrackDataFiles @{1="track1.dat"; 2="track2.dat"} -OutputMkvPath "sample.mkv"

        Reconstructs the MKV sample by combining the SRS structure with pre-extracted track data files.

    .OUTPUTS
        System.Boolean
        Returns $true if reconstruction was successful.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SrsFilePath,

        [Parameter(Mandatory)]
        [hashtable]$TrackDataFiles,

        [Parameter(Mandatory)]
        [string]$OutputMkvPath
    )

    if (-not (Test-Path $SrsFilePath)) {
        throw "SRS file not found: $SrsFilePath"
    }

    try {
        function Get-EbmlVarLength { param([byte]$FirstByte); for ($i = 0; $i -lt 8; $i++) { if (($FirstByte -band (0x80 -shr $i)) -ne 0) { return $i + 1 } }; return 0 }
        function Read-EbmlVarInt { param([System.IO.BinaryReader]$Reader); $firstByte = $Reader.ReadByte(); $bytes = Get-EbmlVarLength $firstByte; [uint64]$mask = 0xFF -shr $bytes; [uint64]$value = ($firstByte -band $mask); $rawBytes = New-Object byte[] $bytes; $rawBytes[0] = $firstByte; for ($i = 1; $i -lt $bytes; $i++) { $rawBytes[$i] = $Reader.ReadByte(); $value = ($value -shl 8) + $rawBytes[$i] }; return @{ Value = $value; Bytes = $bytes; RawBytes = $rawBytes } }
        function Read-EbmlId { param([System.IO.BinaryReader]$Reader); $firstByte = $Reader.ReadByte(); $len = Get-EbmlVarLength $firstByte; $id = New-Object byte[] $len; $id[0] = $firstByte; for ($i = 1; $i -lt $len; $i++) { $id[$i] = $Reader.ReadByte() }; return $id }
        function Compare-Bytes { param([byte[]]$A, [byte[]]$B); if ($null -eq $A -or $null -eq $B) { return $false }; if ($A.Length -ne $B.Length) { return $false }; for ($i = 0; $i -lt $A.Length; $i++) { if ($A[$i] -ne $B[$i]) { return $false } }; return $true }
        function Test-ContainerElement { param([byte[]]$ElemId); if ($null -eq $ElemId) { return $false }; $containers = @([byte[]]@(0x18, 0x53, 0x80, 0x67), [byte[]]@(0x1F, 0x43, 0xB6, 0x75), [byte[]]@(0xA0), [byte[]]@(0x19, 0x41, 0xA4, 0x69), [byte[]]@(0x61, 0xA7)); foreach ($c in $containers) { if (Compare-Bytes -A $ElemId -B $c) { return $true } }; return $false }

        $ID_Resample = [byte[]]@(0x1F, 0x69, 0x75, 0x76); $ID_Block = [byte[]]@(0xA1); $ID_SimpleBlock = [byte[]]@(0xA3)

        $trackDataStreams = [ordered]@{}
        foreach ($key in ($TrackDataFiles.Keys | Sort-Object)) { $trackFile = $TrackDataFiles[$key]; if (Test-Path $trackFile) { $trackDataStreams[$key] = [System.IO.File]::OpenRead($trackFile); Write-Verbose "Opened track data file for track $key : $trackFile" } }

        $srsFs = [System.IO.File]::OpenRead($SrsFilePath)
        $srsReader = [System.IO.BinaryReader]::new($srsFs)
        $srsSize = $srsFs.Length

        $outFs = [System.IO.File]::Create($OutputMkvPath)
        $outWriter = [System.IO.BinaryWriter]::new($outFs)

        try {
            $elemCount = 0; $blockCount = 0; $clusterCount = 0

            while ($srsFs.Position -lt $srsSize) {
                $startPos = $srsFs.Position
                if ($startPos + 2 -gt $srsSize) { break }

                try {
                    $elemId = Read-EbmlId -Reader $srsReader
                    if ($null -eq $elemId -or $elemId.Length -eq 0) { break }
                    $sizeInfo = Read-EbmlVarInt -Reader $srsReader
                    if ($null -eq $sizeInfo -or $null -eq $sizeInfo.RawBytes) { break }
                    $elemSize = $sizeInfo.Value
                    $rawHeader = New-Object byte[] ($elemId.Length + $sizeInfo.RawBytes.Length)
                    [System.Array]::Copy($elemId, 0, $rawHeader, 0, $elemId.Length)
                    [System.Array]::Copy($sizeInfo.RawBytes, 0, $rawHeader, $elemId.Length, $sizeInfo.RawBytes.Length)
                } catch { break }

                if (Compare-Bytes -A $elemId -B $ID_Resample) { $srsFs.Seek($elemSize, [System.IO.SeekOrigin]::Current) | Out-Null; continue }

                if (Test-ContainerElement -ElemId $elemId) {
                    $outWriter.Write($rawHeader, 0, $rawHeader.Length)
                    if (Compare-Bytes -A $elemId -B ([byte[]]@(0x1F, 0x43, 0xB6, 0x75))) { $clusterCount++ }
                    $elemCount++; continue
                }

                if ((Compare-Bytes -A $elemId -B $ID_Block) -or (Compare-Bytes -A $elemId -B $ID_SimpleBlock)) {
                    $blockCount++
                    try {
                        $trackInfo = Read-EbmlVarInt -Reader $srsReader
                        if ($null -eq $trackInfo) { break }
                        $trackNumber = $trackInfo.Value
                        $tcFlags = $srsReader.ReadBytes(3)
                        if ($null -eq $tcFlags -or $tcFlags.Length -lt 3) { break }
                        $flags = $tcFlags[2]; $laceType = ($flags -band 0x06) -shr 1
                        $blockHeaderList = [System.Collections.Generic.List[byte]]::new()
                        foreach ($b in $trackInfo.RawBytes) { $blockHeaderList.Add($b) }
                        foreach ($b in $tcFlags) { $blockHeaderList.Add($b) }

                        if ($laceType -ne 0) {
                            $frameCountByte = $srsReader.ReadByte(); $blockHeaderList.Add($frameCountByte); $frameCount = $frameCountByte + 1
                            if ($laceType -eq 1) { for ($f = 0; $f -lt ($frameCount - 1); $f++) { do { $laceByte = $srsReader.ReadByte(); $blockHeaderList.Add($laceByte) } while ($laceByte -eq 255) } }
                            elseif ($laceType -eq 3) { $firstSizeInfo = Read-EbmlVarInt -Reader $srsReader; foreach ($b in $firstSizeInfo.RawBytes) { $blockHeaderList.Add($b) }; for ($f = 1; $f -lt ($frameCount - 1); $f++) { $deltaInfo = Read-EbmlVarInt -Reader $srsReader; foreach ($b in $deltaInfo.RawBytes) { $blockHeaderList.Add($b) } } }
                        }
                        $blockHeader = $blockHeaderList.ToArray()
                        $frameDataSize = $elemSize - $blockHeader.Length
                        if ($frameDataSize -lt 0) { $frameDataSize = 0 }

                        $outWriter.Write($rawHeader, 0, $rawHeader.Length)
                        $outWriter.Write($blockHeader, 0, $blockHeader.Length)

                        $trackStream = $null
                        foreach ($key in $trackDataStreams.Keys) { if ($key -eq $trackNumber) { $trackStream = $trackDataStreams[$key]; break } }

                        if ($null -ne $trackStream -and $trackStream.Position -lt $trackStream.Length -and $frameDataSize -gt 0) {
                            $frameData = New-Object byte[] $frameDataSize; $bytesRead = $trackStream.Read($frameData, 0, [int]$frameDataSize); $outWriter.Write($frameData, 0, $bytesRead)
                        } elseif ($frameDataSize -gt 0) {
                            $zeros = New-Object byte[] $frameDataSize; $outWriter.Write($zeros, 0, $zeros.Length)
                        }
                    } catch { break }
                    $elemCount++; continue
                }

                $outWriter.Write($rawHeader, 0, $rawHeader.Length)
                $remaining = $srsSize - $srsFs.Position
                $bytesToRead = [Math]::Min([uint64]$elemSize, [uint64]$remaining)
                if ($bytesToRead -gt 0 -and $bytesToRead -le $remaining) {
                    $chunkSize = 1MB; $bytesLeft = $bytesToRead
                    while ($bytesLeft -gt 0) { $toRead = [Math]::Min($chunkSize, $bytesLeft); $chunk = $srsReader.ReadBytes([int]$toRead); if ($chunk.Length -eq 0) { break }; $outWriter.Write($chunk, 0, $chunk.Length); $bytesLeft -= $chunk.Length }
                }
                $elemCount++
            }

            $outWriter.Flush()
            Write-Verbose "Rebuilt complete: $elemCount elements, $blockCount blocks, $clusterCount clusters"
            return $true
        }
        finally {
            foreach ($stream in $trackDataStreams.Values) { if ($null -ne $stream) { $stream.Dispose() } }
            if ($null -ne $outWriter) { $outWriter.Dispose() }
            if ($null -ne $srsReader) { $srsReader.Dispose() }
            if ($null -ne $outFs) { $outFs.Dispose() }
            if ($null -ne $srsFs) { $srsFs.Dispose() }
        }
    }
    catch {
        throw "Failed to rebuild sample: $_"
    }
}