Private/Export-MkvTrackData.ps1

function Export-MkvTrackData {
    <#
    .SYNOPSIS
    Extract track data from main MKV by parsing EBML structure.

    .PARAMETER MainFilePath
    Path to the main MKV file.

    .PARAMETER Tracks
    Hashtable of track metadata from SRS (keyed by track number).

    .PARAMETER OutputFiles
    Hashtable to receive output file paths (keyed by track number).
    #>

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

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

        [Parameter(Mandatory)]
        [hashtable]$OutputFiles
    )

    if (-not (Test-Path $MainFilePath)) {
        throw "Main file not found: $MainFilePath"
    }

    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 }

    $ID_Segment = [byte[]]@(0x18, 0x53, 0x80, 0x67); $ID_Cluster = [byte[]]@(0x1F, 0x43, 0xB6, 0x75); $ID_BlockGroup = [byte[]]@(0xA0); $ID_Block = [byte[]]@(0xA1); $ID_SimpleBlock = [byte[]]@(0xA3)

    try {
        $fs = [System.IO.File]::OpenRead($MainFilePath)
        $reader = [System.IO.BinaryReader]::new($fs)
        $fileSize = $fs.Length

        [uint64]$startOffset = [uint64]::MaxValue
        foreach ($trackNum in $Tracks.Keys) { $track = $Tracks[$trackNum]; if ($track.MatchOffset -gt 0 -and $track.MatchOffset -lt $startOffset) { $startOffset = $track.MatchOffset } }

        $trackStreams = @{}; $trackBytesWritten = @{}
        foreach ($trackNum in $Tracks.Keys) { $tempFile = [System.IO.Path]::GetTempFileName() + ".track$trackNum"; $trackStreams[$trackNum] = [System.IO.File]::Create($tempFile); $trackBytesWritten[$trackNum] = [uint64]0; $OutputFiles[$trackNum] = $tempFile }

        $clusterCount = 0; $blockCount = 0; $done = $false

        $null = Read-EbmlId -Reader $reader; $ebmlSize = Read-EbmlVarInt -Reader $reader; $fs.Seek([int64]$ebmlSize.Value, [System.IO.SeekOrigin]::Current) | Out-Null
        $null = Read-EbmlId -Reader $reader; $null = Read-EbmlVarInt -Reader $reader

        while ($fs.Position -lt $fileSize -and -not $done) {
            $elemStart = $fs.Position
            try { $elemId = Read-EbmlId -Reader $reader; $sizeInfo = Read-EbmlVarInt -Reader $reader; $elemSize = $sizeInfo.Value } catch { break }
            $headerLen = $elemId.Length + $sizeInfo.Bytes

            if (Compare-Bytes -A $elemId -B $ID_Segment) { continue }
            if (Compare-Bytes -A $elemId -B $ID_Cluster) { $clusterCount++; $clusterEnd = $elemStart + $headerLen + $elemSize; if ($clusterEnd -lt $startOffset) { $fs.Seek([int64]$elemSize, [System.IO.SeekOrigin]::Current) | Out-Null; continue }; continue }
            if (Compare-Bytes -A $elemId -B $ID_BlockGroup) { continue }

            if ((Compare-Bytes -A $elemId -B $ID_Block) -or (Compare-Bytes -A $elemId -B $ID_SimpleBlock)) {
                $blockCount++; $blockStart = $fs.Position; $trackInfo = Read-EbmlVarInt -Reader $reader; $trackNumber = [uint16]$trackInfo.Value
                $tcFlags = $reader.ReadBytes(3); $flags = $tcFlags[2]; $laceType = ($flags -band 0x06) -shr 1
                $blockHeaderSize = $trackInfo.Bytes + 3; $frameLengths = @()
                if ($laceType -ne 0) { $frameCountByte = $reader.ReadByte(); $blockHeaderSize++; $frameCount = $frameCountByte + 1; $frameLengths = New-Object int[] $frameCount; $lacingBytesRead = 0
                    if ($laceType -eq 1) { for ($f = 0; $f -lt ($frameCount - 1); $f++) { $frameSize = 0; do { $laceByte = $reader.ReadByte(); $lacingBytesRead++; $frameSize += $laceByte } while ($laceByte -eq 255); $frameLengths[$f] = $frameSize } }
                    elseif ($laceType -eq 3) { $firstSizeInfo = Read-EbmlVarInt -Reader $reader; $lacingBytesRead += $firstSizeInfo.Bytes; $frameLengths[0] = [int]$firstSizeInfo.Value; for ($f = 1; $f -lt ($frameCount - 1); $f++) { $deltaInfo = Read-EbmlVarInt -Reader $reader; $lacingBytesRead += $deltaInfo.Bytes; $delta = [int64]$deltaInfo.Value - ((1 -shl ($deltaInfo.Bytes * 7)) - 1); $frameLengths[$f] = $frameLengths[$f - 1] + [int]$delta } }
                    $blockHeaderSize += $lacingBytesRead
                } else { $frameLengths = @(0) }
                $frameDataSize = [int]$elemSize - $blockHeaderSize
                if ($laceType -eq 2 -and $frameLengths.Count -gt 0) { $frameSize = [int]($frameDataSize / $frameLengths.Count); for ($f = 0; $f -lt $frameLengths.Count; $f++) { $frameLengths[$f] = $frameSize } }
                elseif ($laceType -eq 0) { $frameLengths[0] = $frameDataSize }
                elseif ($laceType -ne 0) { $usedSize = 0; for ($f = 0; $f -lt ($frameLengths.Count - 1); $f++) { $usedSize += $frameLengths[$f] }; $frameLengths[$frameLengths.Count - 1] = $frameDataSize - $usedSize }

                if ($Tracks.ContainsKey($trackNumber)) {
                    $track = $Tracks[$trackNumber]; $trackStream = $trackStreams[$trackNumber]; $frameDataStart = $blockStart + $blockHeaderSize
                    if ($frameDataStart -ge $track.MatchOffset) { if ($trackBytesWritten[$trackNumber] -lt $track.DataLength) { $fs.Seek($frameDataStart, [System.IO.SeekOrigin]::Begin) | Out-Null; $toRead = [Math]::Min($frameDataSize, $track.DataLength - $trackBytesWritten[$trackNumber]); $frameData = $reader.ReadBytes([int]$toRead); $trackStream.Write($frameData, 0, $frameData.Length); $trackBytesWritten[$trackNumber] += $frameData.Length } }
                    $done = $true; foreach ($tNum in $Tracks.Keys) { if ($trackBytesWritten[$tNum] -lt $Tracks[$tNum].DataLength) { $done = $false; break } }
                }
                $blockEnd = $elemStart + $headerLen + $elemSize; $fs.Seek($blockEnd, [System.IO.SeekOrigin]::Begin) | Out-Null; continue
            }
            $fs.Seek([int64]$elemSize, [System.IO.SeekOrigin]::Current) | Out-Null
        }

        foreach ($stream in $trackStreams.Values) { $stream.Flush(); $stream.Dispose() }
        $reader.Dispose(); $fs.Close()
        Write-Verbose "Extracted tracks from $clusterCount clusters, $blockCount blocks"
        return $true
    }
    catch { foreach ($stream in $trackStreams.Values) { if ($null -ne $stream) { $stream.Dispose() } }; throw "Failed to extract MKV track data: $_" }
}