Add support for WebM and MKV output containers with codec-aware stream copying and transcoding
This commit is contained in:
+73
-37
@@ -104,8 +104,47 @@ if (-not (Test-Path -LiteralPath $SourcePath)) {
|
||||
|
||||
$TargetPath = $Target
|
||||
|
||||
if (-not $TargetPath.EndsWith('.mp4', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
# Determine output container from the target extension.
|
||||
# Supported: .mp4 (H.264/AAC), .webm (VP9/Opus), .mkv (H.264/AAC, permissive copy).
|
||||
# If no recognised extension is provided, default to .mp4 (preserves prior behaviour).
|
||||
$supportedTargetExts = @('.mp4', '.webm', '.mkv')
|
||||
$currentTargetExt = [System.IO.Path]::GetExtension($TargetPath).ToLowerInvariant()
|
||||
if ($supportedTargetExts -notcontains $currentTargetExt) {
|
||||
$TargetPath += '.mp4'
|
||||
$currentTargetExt = '.mp4'
|
||||
}
|
||||
$targetContainer = $currentTargetExt.TrimStart('.')
|
||||
|
||||
# Output-container profile: target codecs, ffmpeg encode args, and the list of
|
||||
# source codecs that can be stream-copied directly into the container without
|
||||
# re-encoding.
|
||||
switch ($targetContainer) {
|
||||
'webm' {
|
||||
$targetVideoCodec = 'libvpx-vp9'
|
||||
$targetVideoEncodeArgs = @('-c:v', 'libvpx-vp9', '-pix_fmt', 'yuv420p', '-deadline', 'good', '-cpu-used', '2', '-row-mt', '1')
|
||||
$targetAudioCodec = 'libopus'
|
||||
$targetAudioBitrate = '160k'
|
||||
$videoCopyCodecs = @('vp8', 'vp9', 'av1')
|
||||
$audioCopyCodecs = @('opus', 'vorbis')
|
||||
}
|
||||
'mkv' {
|
||||
$targetVideoCodec = 'libx264'
|
||||
$targetVideoEncodeArgs = @('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow')
|
||||
$targetAudioCodec = 'aac'
|
||||
$targetAudioBitrate = '192k'
|
||||
# MKV is permissive: stream-copy almost any common codec.
|
||||
$videoCopyCodecs = @('h264', 'hevc', 'vp8', 'vp9', 'av1', 'mpeg4')
|
||||
$audioCopyCodecs = @('aac', 'mp3', 'ac3', 'eac3', 'alac', 'flac', 'opus', 'vorbis')
|
||||
}
|
||||
default {
|
||||
# 'mp4' fallback
|
||||
$targetVideoCodec = 'libx264'
|
||||
$targetVideoEncodeArgs = @('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow')
|
||||
$targetAudioCodec = 'aac'
|
||||
$targetAudioBitrate = '192k'
|
||||
$videoCopyCodecs = @('h264')
|
||||
$audioCopyCodecs = @('aac', 'mp3', 'ac3', 'eac3', 'alac')
|
||||
}
|
||||
}
|
||||
|
||||
$TargetDir = Split-Path -Parent $TargetPath
|
||||
@@ -129,9 +168,9 @@ if (-not [string]::IsNullOrWhiteSpace($Audio)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Probe the external audio file's codec so a non-MP4-compatible codec
|
||||
# (e.g. Opus from a YouTube/DASH download) is transcoded to AAC instead
|
||||
# of being stream-copied into the MP4 container.
|
||||
# Probe the external audio file's codec so a codec that's not compatible with
|
||||
# the target container (e.g. Opus into MP4, or AAC into WebM) is transcoded
|
||||
# rather than stream-copied.
|
||||
$ffprobeExtAudioOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $AudioPath 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$externalAudioCodec = ($ffprobeExtAudioOutput | Select-Object -First 1).Trim().ToLowerInvariant()
|
||||
@@ -217,9 +256,9 @@ if ($ffprobeExit -eq 0) {
|
||||
}
|
||||
}
|
||||
|
||||
# Probe source video and audio codecs. The MP4 container only supports a limited set
|
||||
# of codecs: when the source is VP9/VP8/AV1 video or Opus/Vorbis/etc. audio (typical
|
||||
# YouTube/Google DASH downloads), we must transcode rather than stream-copy.
|
||||
# Probe source video and audio codecs. When the source codec isn't on the
|
||||
# target container's stream-copy list, the script will transcode to the
|
||||
# container's chosen codec (see container profile above).
|
||||
$ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
|
||||
$sourceVideoCodec = ''
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
@@ -232,14 +271,11 @@ if ($LASTEXITCODE -eq 0) {
|
||||
$sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant()
|
||||
}
|
||||
|
||||
# A non-H.264 source forces a video re-encode regardless of trimming/scaling/FPS changes.
|
||||
$videoCodecMismatch = ($sourceVideoCodec -and $sourceVideoCodec -ne 'h264')
|
||||
|
||||
# MP4-container-compatible audio codecs. Anything else (opus, vorbis, flac, ...) must
|
||||
# be transcoded to AAC when stream-copying audio.
|
||||
$mp4AudioCodecs = @('aac', 'mp3', 'ac3', 'eac3', 'alac')
|
||||
$audioCodecMismatch = ($sourceAudioCodec -and ($mp4AudioCodecs -notcontains $sourceAudioCodec))
|
||||
$externalAudioCodecMismatch = ($externalAudioCodec -and ($mp4AudioCodecs -notcontains $externalAudioCodec))
|
||||
# A source codec not on the target container's stream-copy list forces a re-encode
|
||||
# regardless of trimming/scaling/FPS changes.
|
||||
$videoCodecMismatch = ($sourceVideoCodec -and ($videoCopyCodecs -notcontains $sourceVideoCodec))
|
||||
$audioCodecMismatch = ($sourceAudioCodec -and ($audioCopyCodecs -notcontains $sourceAudioCodec))
|
||||
$externalAudioCodecMismatch = ($externalAudioCodec -and ($audioCopyCodecs -notcontains $externalAudioCodec))
|
||||
|
||||
if ($StartFrame -lt 0 -or $StartFrame -gt $maxFrameIndex) {
|
||||
Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $maxFrameIndex)
|
||||
@@ -310,15 +346,16 @@ if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
|
||||
$frameCount = $actualEndFrame - $StartFrame + 1
|
||||
|
||||
Write-Host ('Cloning video: {0}' -f $SourcePath)
|
||||
Write-Host (' Target container: {0} (video={1} audio={2})' -f $targetContainer, $targetVideoCodec, $targetAudioCodec)
|
||||
if ($sourceVideoCodec) {
|
||||
Write-Host (' Source codec: video={0}{1} audio={2}{3}' -f `
|
||||
$sourceVideoCodec, $(if ($videoCodecMismatch) { ' (will transcode to h264)' } else { '' }), `
|
||||
$sourceVideoCodec, $(if ($videoCodecMismatch) { " (will transcode to $targetVideoCodec)" } else { '' }), `
|
||||
$(if ($sourceAudioCodec) { $sourceAudioCodec } else { 'none' }), `
|
||||
$(if ($audioCodecMismatch) { ' (will transcode to aac)' } else { '' }))
|
||||
$(if ($audioCodecMismatch) { " (will transcode to $targetAudioCodec)" } else { '' }))
|
||||
}
|
||||
if ($AudioPath -and $externalAudioCodec) {
|
||||
Write-Host (' External audio codec: {0}{1}' -f `
|
||||
$externalAudioCodec, $(if ($externalAudioCodecMismatch) { ' (will transcode to aac)' } else { '' }))
|
||||
$externalAudioCodec, $(if ($externalAudioCodecMismatch) { " (will transcode to $targetAudioCodec)" } else { '' }))
|
||||
}
|
||||
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount)
|
||||
if ($scaleWidth -gt 0) {
|
||||
@@ -353,16 +390,17 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
||||
$ffmpegArgs += @(
|
||||
'-vf', $videoFilter
|
||||
'-vsync', 'vfr'
|
||||
'-c:v', 'libx264'
|
||||
'-pix_fmt', 'yuv420p'
|
||||
'-preset', 'slow'
|
||||
)
|
||||
$ffmpegArgs += $targetVideoEncodeArgs
|
||||
if ($CRF -ge 0) {
|
||||
$ffmpegArgs += @('-crf', $CRF.ToString())
|
||||
# libvpx-vp9 ignores -crf unless -b:v 0 is also set (constant-quality mode).
|
||||
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||
} elseif ($videoBitrate) {
|
||||
$ffmpegArgs += @('-b:v', $videoBitrate)
|
||||
} else {
|
||||
$ffmpegArgs += @('-crf', '14')
|
||||
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||
}
|
||||
} elseif ($needsReencode) {
|
||||
$filterParts = @()
|
||||
@@ -376,17 +414,15 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
||||
$videoFilter = $filterParts -join ','
|
||||
$ffmpegArgs += @('-vf', $videoFilter)
|
||||
}
|
||||
$ffmpegArgs += @(
|
||||
'-c:v', 'libx264'
|
||||
'-pix_fmt', 'yuv420p'
|
||||
'-preset', 'slow'
|
||||
)
|
||||
$ffmpegArgs += $targetVideoEncodeArgs
|
||||
if ($CRF -ge 0) {
|
||||
$ffmpegArgs += @('-crf', $CRF.ToString())
|
||||
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||
} elseif ($videoBitrate) {
|
||||
$ffmpegArgs += @('-b:v', $videoBitrate)
|
||||
} else {
|
||||
$ffmpegArgs += @('-crf', '14')
|
||||
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||
}
|
||||
} else {
|
||||
$ffmpegArgs += @('-c:v', 'copy')
|
||||
@@ -404,15 +440,16 @@ if ($NoAudio) {
|
||||
'-map', '0:v'
|
||||
'-map', '1:a'
|
||||
'-t', $durationStr
|
||||
'-c:a', 'aac'
|
||||
'-c:a', $targetAudioCodec
|
||||
'-b:a', $targetAudioBitrate
|
||||
)
|
||||
} elseif ($externalAudioCodecMismatch) {
|
||||
# External audio (e.g. Opus from a YouTube/DASH WebM) cannot be stream-copied into MP4.
|
||||
# External audio cannot be stream-copied into the target container.
|
||||
$ffmpegArgs += @(
|
||||
'-map', '0:v'
|
||||
'-map', '1:a'
|
||||
'-c:a', 'aac'
|
||||
'-b:a', '192k'
|
||||
'-c:a', $targetAudioCodec
|
||||
'-b:a', $targetAudioBitrate
|
||||
'-shortest'
|
||||
)
|
||||
} else {
|
||||
@@ -431,14 +468,13 @@ if ($NoAudio) {
|
||||
$endTimeStr = $endTime.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
$audioFilter = "atrim=start=${startTimeStr}:end=${endTimeStr},asetpts=PTS-STARTPTS"
|
||||
$ffmpegArgs += @('-af', $audioFilter)
|
||||
if ($audioCodecMismatch) {
|
||||
# ffmpeg will already re-encode here (no -c:a copy), but force AAC explicitly
|
||||
# so the output is MP4-compatible regardless of source codec.
|
||||
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
|
||||
}
|
||||
# The atrim filter forces an audio re-encode; explicitly set the target
|
||||
# container's audio codec so the result is always container-appropriate
|
||||
# (e.g. Opus for WebM, AAC for MP4/MKV) rather than ffmpeg's default.
|
||||
$ffmpegArgs += @('-c:a', $targetAudioCodec, '-b:a', $targetAudioBitrate)
|
||||
} elseif ($audioCodecMismatch) {
|
||||
# Source audio (e.g. Opus from a YouTube/DASH WebM) cannot be stream-copied into MP4.
|
||||
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
|
||||
# Source audio is not on the target container's stream-copy list.
|
||||
$ffmpegArgs += @('-c:a', $targetAudioCodec, '-b:a', $targetAudioBitrate)
|
||||
} else {
|
||||
$ffmpegArgs += @('-c:a', 'copy')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user