Public/Build-SampleAviFromSrs.ps1
|
function Build-SampleAviFromSrs { <# .SYNOPSIS Reconstructs an AVI sample file from an SRS file and source video. .DESCRIPTION AVI SRS files store the structure of the original sample (frame headers and their sizes) without the actual frame data. This function: 1. Parses the SRS to get track metadata (match offsets in source) 2. Parses the source AVI movi structure to find matching chunks 3. Copies AVI structure from SRS and injects frame data from source .PARAMETER SrsData Raw bytes of the SRS file. .PARAMETER SourcePath Path to the source AVI file containing the full movie. .PARAMETER OutputPath Path for the reconstructed sample AVI file. .EXAMPLE Build-SampleAviFromSrs -SrsData $srsBytes -SourcePath "movie.avi" -OutputPath "sample.avi" Reconstructs the AVI sample file from the SRS metadata and source video. .OUTPUTS System.Boolean Returns $true if reconstruction was successful. #> [CmdletBinding()] param( [Parameter(Mandatory)] [byte[]]$SrsData, [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory)] [string]$OutputPath ) if (-not (Test-Path $SourcePath)) { throw "Source file not found: $SourcePath" } # Parse SRS metadata $srsInfo = ConvertFrom-SrsAviFile -Data $SrsData if (-not $srsInfo.FileMetadata) { throw "Failed to parse SRS metadata" } Write-Verbose "SRS Info: $($srsInfo.FileMetadata.FileName), expected size: $($srsInfo.FileMetadata.FileSize)" Write-Verbose "Tracks: $($srsInfo.Tracks.Count)" foreach ($trackNum in $srsInfo.Tracks.Keys | Sort-Object) { $track = $srsInfo.Tracks[$trackNum] Write-Verbose " Track $trackNum : $($track.DataLength) bytes at offset $($track.MatchOffset)" } # Parse source AVI to build an index of movi chunks Write-Verbose "Parsing source AVI movi structure..." $sourceChunks = Get-AviMoviChunks -FilePath $SourcePath # Find the minimum match offset to determine where the sample region starts $minMatchOffset = [long]::MaxValue foreach ($trackNum in $srsInfo.Tracks.Keys) { $track = $srsInfo.Tracks[$trackNum] if ($track.MatchOffset -lt $minMatchOffset) { $minMatchOffset = $track.MatchOffset } } # Get all chunks for each track starting from the sample region # Don't limit by dataLength - interleaving means the range is much larger $trackChunks = @{} foreach ($trackNum in $srsInfo.Tracks.Keys) { $track = $srsInfo.Tracks[$trackNum] $matchOffset = $track.MatchOffset $dataLength = $track.DataLength # Find the first chunk that contains or starts at the match offset $startChunk = $sourceChunks | Where-Object { $_.StreamNum -eq $trackNum -and $_.DataOffset -le $matchOffset -and ($_.DataOffset + $_.Size) -gt $matchOffset } | Select-Object -First 1 if (-not $startChunk) { # Match offset is exactly at start of a chunk $startChunk = $sourceChunks | Where-Object { $_.StreamNum -eq $trackNum -and $_.DataOffset -ge $matchOffset } | Sort-Object DataOffset | Select-Object -First 1 } # Get all chunks for this track starting from the start chunk $startOffset = if ($startChunk) { $startChunk.DataOffset } else { $matchOffset } $chunks = $sourceChunks | Where-Object { $_.StreamNum -eq $trackNum -and $_.DataOffset -ge $startOffset } | Sort-Object DataOffset # Calculate initial skip (if match_offset is inside first chunk) $initialSkip = [long]0 if ($startChunk -and $matchOffset -gt $startChunk.DataOffset) { $initialSkip = $matchOffset - $startChunk.DataOffset } $trackChunks[$trackNum] = @{ StartOffset = $matchOffset DataLength = $dataLength Chunks = @($chunks) InitialSkip = $initialSkip } Write-Verbose " Track $trackNum : found $($chunks.Count) chunks in source (initialSkip=$initialSkip)" } # Open source file $sourceFs = [System.IO.File]::OpenRead($SourcePath) $sourceReader = [System.IO.BinaryReader]::new($sourceFs) # Create output file $outFs = [System.IO.File]::Create($OutputPath) $outWriter = [System.IO.BinaryWriter]::new($outFs) try { $ms = [System.IO.MemoryStream]::new($SrsData) $reader = [System.IO.BinaryReader]::new($ms) # Track how much data we've read from each track # Use [int] keys consistently to avoid type mismatch in lookups $trackBytesRead = @{} foreach ($trackNum in $srsInfo.Tracks.Keys) { $trackBytesRead[[int]$trackNum] = [long]0 } # Build a lookup for source chunks by track and cumulative offset # Account for initialSkip in the first chunk # Use [int] keys consistently to avoid type mismatch in lookups $trackChunkIndex = @{} foreach ($trackNum in $srsInfo.Tracks.Keys) { $info = $trackChunks[$trackNum] $intTrackNum = [int]$trackNum $cumulative = [long]0 $chunkList = [System.Collections.ArrayList]::new() $initialSkip = $info.InitialSkip $isFirst = $true foreach ($chunk in ($info.Chunks | Sort-Object DataOffset)) { $chunkUsableStart = 0 $chunkUsableSize = $chunk.Size if ($isFirst -and $initialSkip -gt 0) { # First chunk starts at initialSkip $chunkUsableStart = $initialSkip $chunkUsableSize = $chunk.Size - $initialSkip $isFirst = $false } if ($chunkUsableSize -gt 0) { [void]$chunkList.Add(@{ Chunk = $chunk ChunkDataStart = $chunkUsableStart # Offset within chunk to start reading CumulativeStart = $cumulative CumulativeEnd = $cumulative + $chunkUsableSize }) $cumulative += $chunkUsableSize } } $trackChunkIndex[$intTrackNum] = $chunkList # Debug: show first few entries if ($chunkList.Count -gt 0) { $first = $chunkList[0] Write-Verbose " First entry: Chunk at $($first.Chunk.DataOffset), ChunkDataStart=$($first.ChunkDataStart), Cumulative=[$($first.CumulativeStart),$($first.CumulativeEnd)]" } } # Copy RIFF header (12 bytes: "RIFF" + size + "AVI ") $outWriter.Write($reader.ReadBytes(12)) Write-Verbose "Wrote RIFF header" # Process chunks until movi while ($ms.Position -lt $SrsData.Length - 8) { $chunkId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $chunkSize = $reader.ReadUInt32() if ($chunkId -eq 'LIST') { $listType = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) if ($listType -eq 'movi') { # Write movi LIST header with original declared size $outWriter.Write([byte[]]@(0x4C, 0x49, 0x53, 0x54)) # "LIST" $outWriter.Write([BitConverter]::GetBytes($chunkSize)) $outWriter.Write([byte[]]@(0x6D, 0x6F, 0x76, 0x69)) # "movi" Write-Verbose "Writing movi LIST (declared size: $chunkSize)" # Skip SRSF and SRST chunks to find frame index while ($ms.Position -lt $SrsData.Length - 8) { $subId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $subSize = $reader.ReadUInt32() if ($subId -eq 'SRSF' -or $subId -eq 'SRST') { # Skip ReSample metadata chunks $ms.Seek($subSize, [System.IO.SeekOrigin]::Current) | Out-Null if ($ms.Position % 2 -eq 1) { $ms.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null } continue } # Seek back to read the first frame header $ms.Seek(-8, [System.IO.SeekOrigin]::Current) | Out-Null # Process frame headers (8 bytes each: FOURCC + size) $frameCount = 0 while ($ms.Position -lt $SrsData.Length - 8) { # Check for padding or end $peekByte = $SrsData[$ms.Position] if ($peekByte -eq 0) { $ms.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null continue } $frameId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $frameSize = $reader.ReadUInt32() if ($frameId -eq 'idx1') { $ms.Seek(-8, [System.IO.SeekOrigin]::Current) | Out-Null break } if ($frameId -notmatch '^(\d\d)(d[cb]|wb)$') { Write-Verbose "Unknown frame type '$frameId' at position $($ms.Position - 8), stopping" break } $streamNum = [int]$Matches[1] # Write chunk header to output $outWriter.Write([System.Text.Encoding]::ASCII.GetBytes($frameId)) $outWriter.Write([BitConverter]::GetBytes([uint32]$frameSize)) # Find the source chunk containing this frame's data if ($frameSize -gt 0 -and $trackChunkIndex.ContainsKey($streamNum)) { $currentOffset = $trackBytesRead[$streamNum] $targetEnd = $currentOffset + $frameSize # Find chunks that contain this data range $bytesWritten = 0 foreach ($entry in $trackChunkIndex[$streamNum]) { if ($entry.CumulativeEnd -le $currentOffset) { continue } if ($entry.CumulativeStart -ge $targetEnd) { break } $chunk = $entry.Chunk $chunkBaseOffset = $entry.ChunkDataStart # Offset within chunk where usable data starts # Calculate how much to read from this chunk # offsetInEntry = position within this chunk entry's usable range $offsetInEntry = [Math]::Max(0, $currentOffset - $entry.CumulativeStart) $entryUsableSize = $entry.CumulativeEnd - $entry.CumulativeStart $endInEntry = [Math]::Min($entryUsableSize, $targetEnd - $entry.CumulativeStart) $bytesToRead = $endInEntry - $offsetInEntry if ($bytesToRead -gt 0) { # File position = chunk's data offset + base offset (for first chunk skip) + position in entry $filePos = $chunk.DataOffset + $chunkBaseOffset + $offsetInEntry $sourceFs.Seek($filePos, [System.IO.SeekOrigin]::Begin) | Out-Null $data = $sourceReader.ReadBytes([int]$bytesToRead) $outWriter.Write($data) $bytesWritten += $data.Length } } if ($bytesWritten -lt $frameSize) { # Pad with zeros if we couldn't find all data $zeros = New-Object byte[] ($frameSize - $bytesWritten) $outWriter.Write($zeros) } $trackBytesRead[$streamNum] = $targetEnd } elseif ($frameSize -gt 0) { $zeros = New-Object byte[] $frameSize $outWriter.Write($zeros) } # Word alignment padding in output if ($frameSize % 2 -eq 1) { $outWriter.Write([byte]0) } $frameCount++ } Write-Verbose "Processed $frameCount frames" break } # Find and copy idx1 chunk and trailing chunks for ($i = [int]$ms.Position; $i -lt $SrsData.Length - 4; $i++) { if ($SrsData[$i] -eq 0x69 -and $SrsData[$i + 1] -eq 0x64 -and $SrsData[$i + 2] -eq 0x78 -and $SrsData[$i + 3] -eq 0x31) { $ms.Seek($i, [System.IO.SeekOrigin]::Begin) | Out-Null Write-Verbose "Found idx1 at position $i" $idx1Id = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $idx1Size = $reader.ReadUInt32() $outWriter.Write([System.Text.Encoding]::ASCII.GetBytes($idx1Id)) $outWriter.Write([BitConverter]::GetBytes($idx1Size)) $outWriter.Write($reader.ReadBytes([int]$idx1Size)) Write-Verbose "Copied idx1: $idx1Size bytes" # Copy trailing chunks while ($ms.Position -lt $SrsData.Length - 8) { $trailId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $trailSz = $reader.ReadUInt32() if ($trailId -match '^[A-Za-z0-9 ]{4}$' -and $trailSz -lt 100000) { $outWriter.Write([System.Text.Encoding]::ASCII.GetBytes($trailId)) $outWriter.Write([BitConverter]::GetBytes($trailSz)) $bytesToRead = [Math]::Min($trailSz, $SrsData.Length - $ms.Position) if ($bytesToRead -gt 0) { $outWriter.Write($reader.ReadBytes([int]$bytesToRead)) } Write-Verbose "Copied trailing chunk '$trailId': $trailSz bytes" } else { break } } break } } break } else { $outWriter.Write([byte[]]@(0x4C, 0x49, 0x53, 0x54)) $outWriter.Write([BitConverter]::GetBytes($chunkSize)) $outWriter.Write([System.Text.Encoding]::ASCII.GetBytes($listType)) $contentSize = $chunkSize - 4 $outWriter.Write($reader.ReadBytes([int]$contentSize)) Write-Verbose "Copied LIST '$listType': $chunkSize bytes" } } else { $outWriter.Write([System.Text.Encoding]::ASCII.GetBytes($chunkId)) $outWriter.Write([BitConverter]::GetBytes($chunkSize)) $outWriter.Write($reader.ReadBytes([int]$chunkSize)) Write-Verbose "Copied chunk '$chunkId': $chunkSize bytes" } if ($ms.Position % 2 -eq 1) { $ms.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null } } $outWriter.Flush() Write-Verbose "Reconstruction complete: $($outFs.Length) bytes" Write-Verbose "Expected size: $($srsInfo.FileMetadata.FileSize) bytes" $reader.Dispose() $ms.Dispose() return $true } catch { throw "Failed to rebuild sample: $_" } finally { if ($null -ne $outWriter) { $outWriter.Dispose() } if ($null -ne $outFs) { $outFs.Dispose() } if ($null -ne $sourceReader) { $sourceReader.Dispose() } if ($null -ne $sourceFs) { $sourceFs.Dispose() } } } function Get-AviMoviChunks { <# .SYNOPSIS Parses an AVI file and returns all movi chunks with their positions. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$FilePath ) $chunks = [System.Collections.ArrayList]::new() $fs = [System.IO.File]::OpenRead($FilePath) $reader = [System.IO.BinaryReader]::new($fs) try { # Skip RIFF header $fs.Seek(12, [System.IO.SeekOrigin]::Begin) | Out-Null # Find movi LIST while ($fs.Position -lt $fs.Length - 12) { $chunkPos = $fs.Position $chunkId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $chunkSize = $reader.ReadUInt32() if ($chunkId -eq 'LIST') { $listType = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) if ($listType -eq 'movi') { # Parse movi chunks $moviEnd = $chunkPos + 8 + $chunkSize while ($fs.Position -lt $moviEnd - 8) { $subPos = $fs.Position $subId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4)) $subSize = $reader.ReadUInt32() $dataPos = $fs.Position if ($subId -match '^(\d\d)(d[cb]|wb)$') { $streamNum = [int]$Matches[1] [void]$chunks.Add([PSCustomObject]@{ FourCC = $subId StreamNum = $streamNum ChunkOffset = $subPos DataOffset = $dataPos Size = $subSize }) } # Skip chunk data $fs.Seek($subSize, [System.IO.SeekOrigin]::Current) | Out-Null if ($fs.Position % 2 -eq 1) { $fs.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null } } break } else { $fs.Seek($chunkSize - 4, [System.IO.SeekOrigin]::Current) | Out-Null } } else { $fs.Seek($chunkSize, [System.IO.SeekOrigin]::Current) | Out-Null } if ($fs.Position % 2 -eq 1) { $fs.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null } } } finally { $reader.Dispose() $fs.Dispose() } return $chunks } |