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
|
$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'
|
$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
|
$TargetDir = Split-Path -Parent $TargetPath
|
||||||
@@ -129,9 +168,9 @@ if (-not [string]::IsNullOrWhiteSpace($Audio)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Probe the external audio file's codec so a non-MP4-compatible codec
|
# Probe the external audio file's codec so a codec that's not compatible with
|
||||||
# (e.g. Opus from a YouTube/DASH download) is transcoded to AAC instead
|
# the target container (e.g. Opus into MP4, or AAC into WebM) is transcoded
|
||||||
# of being stream-copied into the MP4 container.
|
# 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
|
$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) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$externalAudioCodec = ($ffprobeExtAudioOutput | Select-Object -First 1).Trim().ToLowerInvariant()
|
$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
|
# Probe source video and audio codecs. When the source codec isn't on the
|
||||||
# of codecs: when the source is VP9/VP8/AV1 video or Opus/Vorbis/etc. audio (typical
|
# target container's stream-copy list, the script will transcode to the
|
||||||
# YouTube/Google DASH downloads), we must transcode rather than stream-copy.
|
# 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
|
$ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
|
||||||
$sourceVideoCodec = ''
|
$sourceVideoCodec = ''
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
@@ -232,14 +271,11 @@ if ($LASTEXITCODE -eq 0) {
|
|||||||
$sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant()
|
$sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
# A non-H.264 source forces a video re-encode regardless of trimming/scaling/FPS changes.
|
# A source codec not on the target container's stream-copy list forces a re-encode
|
||||||
$videoCodecMismatch = ($sourceVideoCodec -and $sourceVideoCodec -ne 'h264')
|
# regardless of trimming/scaling/FPS changes.
|
||||||
|
$videoCodecMismatch = ($sourceVideoCodec -and ($videoCopyCodecs -notcontains $sourceVideoCodec))
|
||||||
# MP4-container-compatible audio codecs. Anything else (opus, vorbis, flac, ...) must
|
$audioCodecMismatch = ($sourceAudioCodec -and ($audioCopyCodecs -notcontains $sourceAudioCodec))
|
||||||
# be transcoded to AAC when stream-copying audio.
|
$externalAudioCodecMismatch = ($externalAudioCodec -and ($audioCopyCodecs -notcontains $externalAudioCodec))
|
||||||
$mp4AudioCodecs = @('aac', 'mp3', 'ac3', 'eac3', 'alac')
|
|
||||||
$audioCodecMismatch = ($sourceAudioCodec -and ($mp4AudioCodecs -notcontains $sourceAudioCodec))
|
|
||||||
$externalAudioCodecMismatch = ($externalAudioCodec -and ($mp4AudioCodecs -notcontains $externalAudioCodec))
|
|
||||||
|
|
||||||
if ($StartFrame -lt 0 -or $StartFrame -gt $maxFrameIndex) {
|
if ($StartFrame -lt 0 -or $StartFrame -gt $maxFrameIndex) {
|
||||||
Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $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
|
$frameCount = $actualEndFrame - $StartFrame + 1
|
||||||
|
|
||||||
Write-Host ('Cloning video: {0}' -f $SourcePath)
|
Write-Host ('Cloning video: {0}' -f $SourcePath)
|
||||||
|
Write-Host (' Target container: {0} (video={1} audio={2})' -f $targetContainer, $targetVideoCodec, $targetAudioCodec)
|
||||||
if ($sourceVideoCodec) {
|
if ($sourceVideoCodec) {
|
||||||
Write-Host (' Source codec: video={0}{1} audio={2}{3}' -f `
|
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 ($sourceAudioCodec) { $sourceAudioCodec } else { 'none' }), `
|
||||||
$(if ($audioCodecMismatch) { ' (will transcode to aac)' } else { '' }))
|
$(if ($audioCodecMismatch) { " (will transcode to $targetAudioCodec)" } else { '' }))
|
||||||
}
|
}
|
||||||
if ($AudioPath -and $externalAudioCodec) {
|
if ($AudioPath -and $externalAudioCodec) {
|
||||||
Write-Host (' External audio codec: {0}{1}' -f `
|
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)
|
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount)
|
||||||
if ($scaleWidth -gt 0) {
|
if ($scaleWidth -gt 0) {
|
||||||
@@ -353,16 +390,17 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
|||||||
$ffmpegArgs += @(
|
$ffmpegArgs += @(
|
||||||
'-vf', $videoFilter
|
'-vf', $videoFilter
|
||||||
'-vsync', 'vfr'
|
'-vsync', 'vfr'
|
||||||
'-c:v', 'libx264'
|
|
||||||
'-pix_fmt', 'yuv420p'
|
|
||||||
'-preset', 'slow'
|
|
||||||
)
|
)
|
||||||
|
$ffmpegArgs += $targetVideoEncodeArgs
|
||||||
if ($CRF -ge 0) {
|
if ($CRF -ge 0) {
|
||||||
$ffmpegArgs += @('-crf', $CRF.ToString())
|
$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) {
|
} elseif ($videoBitrate) {
|
||||||
$ffmpegArgs += @('-b:v', $videoBitrate)
|
$ffmpegArgs += @('-b:v', $videoBitrate)
|
||||||
} else {
|
} else {
|
||||||
$ffmpegArgs += @('-crf', '14')
|
$ffmpegArgs += @('-crf', '14')
|
||||||
|
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||||
}
|
}
|
||||||
} elseif ($needsReencode) {
|
} elseif ($needsReencode) {
|
||||||
$filterParts = @()
|
$filterParts = @()
|
||||||
@@ -376,17 +414,15 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
|||||||
$videoFilter = $filterParts -join ','
|
$videoFilter = $filterParts -join ','
|
||||||
$ffmpegArgs += @('-vf', $videoFilter)
|
$ffmpegArgs += @('-vf', $videoFilter)
|
||||||
}
|
}
|
||||||
$ffmpegArgs += @(
|
$ffmpegArgs += $targetVideoEncodeArgs
|
||||||
'-c:v', 'libx264'
|
|
||||||
'-pix_fmt', 'yuv420p'
|
|
||||||
'-preset', 'slow'
|
|
||||||
)
|
|
||||||
if ($CRF -ge 0) {
|
if ($CRF -ge 0) {
|
||||||
$ffmpegArgs += @('-crf', $CRF.ToString())
|
$ffmpegArgs += @('-crf', $CRF.ToString())
|
||||||
|
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||||
} elseif ($videoBitrate) {
|
} elseif ($videoBitrate) {
|
||||||
$ffmpegArgs += @('-b:v', $videoBitrate)
|
$ffmpegArgs += @('-b:v', $videoBitrate)
|
||||||
} else {
|
} else {
|
||||||
$ffmpegArgs += @('-crf', '14')
|
$ffmpegArgs += @('-crf', '14')
|
||||||
|
if ($targetVideoCodec -eq 'libvpx-vp9') { $ffmpegArgs += @('-b:v', '0') }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$ffmpegArgs += @('-c:v', 'copy')
|
$ffmpegArgs += @('-c:v', 'copy')
|
||||||
@@ -404,15 +440,16 @@ if ($NoAudio) {
|
|||||||
'-map', '0:v'
|
'-map', '0:v'
|
||||||
'-map', '1:a'
|
'-map', '1:a'
|
||||||
'-t', $durationStr
|
'-t', $durationStr
|
||||||
'-c:a', 'aac'
|
'-c:a', $targetAudioCodec
|
||||||
|
'-b:a', $targetAudioBitrate
|
||||||
)
|
)
|
||||||
} elseif ($externalAudioCodecMismatch) {
|
} 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 += @(
|
$ffmpegArgs += @(
|
||||||
'-map', '0:v'
|
'-map', '0:v'
|
||||||
'-map', '1:a'
|
'-map', '1:a'
|
||||||
'-c:a', 'aac'
|
'-c:a', $targetAudioCodec
|
||||||
'-b:a', '192k'
|
'-b:a', $targetAudioBitrate
|
||||||
'-shortest'
|
'-shortest'
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -431,14 +468,13 @@ if ($NoAudio) {
|
|||||||
$endTimeStr = $endTime.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
|
$endTimeStr = $endTime.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||||
$audioFilter = "atrim=start=${startTimeStr}:end=${endTimeStr},asetpts=PTS-STARTPTS"
|
$audioFilter = "atrim=start=${startTimeStr}:end=${endTimeStr},asetpts=PTS-STARTPTS"
|
||||||
$ffmpegArgs += @('-af', $audioFilter)
|
$ffmpegArgs += @('-af', $audioFilter)
|
||||||
if ($audioCodecMismatch) {
|
# The atrim filter forces an audio re-encode; explicitly set the target
|
||||||
# ffmpeg will already re-encode here (no -c:a copy), but force AAC explicitly
|
# container's audio codec so the result is always container-appropriate
|
||||||
# so the output is MP4-compatible regardless of source codec.
|
# (e.g. Opus for WebM, AAC for MP4/MKV) rather than ffmpeg's default.
|
||||||
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
|
$ffmpegArgs += @('-c:a', $targetAudioCodec, '-b:a', $targetAudioBitrate)
|
||||||
}
|
|
||||||
} elseif ($audioCodecMismatch) {
|
} elseif ($audioCodecMismatch) {
|
||||||
# Source audio (e.g. Opus from a YouTube/DASH WebM) cannot be stream-copied into MP4.
|
# Source audio is not on the target container's stream-copy list.
|
||||||
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
|
$ffmpegArgs += @('-c:a', $targetAudioCodec, '-b:a', $targetAudioBitrate)
|
||||||
} else {
|
} else {
|
||||||
$ffmpegArgs += @('-c:a', 'copy')
|
$ffmpegArgs += @('-c:a', 'copy')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user