spav1an/spav1an.ps1

463 lines
19 KiB
PowerShell

param (
[String] $EncoderPath = "aomenc-3.0.0-335.exe",
[String] $FFmpegPath = "ffmpeg.exe",
[String] $FFprobePath = "ffprobe.exe",
[Switch] $PrintCommands,
[int] $EncoderType = 1, # Encoder type. 1 = aomenc, 2 = SVT-AV1
[int] $Renderer = 1, # Frame type, 1 = ffmpeg native, 2 = avisynth
[int] $MinChunkSec = 10,
[int] $Threads = 0,
[String] $Framerate = "auto",
[String] $AnalyzeVidSize = "848x480",
[String] $AnalyzeVidEnc = "-hide_banner -hide_banner -loglevel error -stats -i {0} -map 0:0 -vf zscale={1}:f=spline36 -vcodec libx264 -preset veryfast -crf 40 -x264-params keyint={2}:min-keyint=5:scenecut=60 -y -pix_fmt yuv420p {3}",
[String] $AvisynthScript = "LSMASHVideoSource({0})",
[String] $EncoderOptions = "--cpu-used=8 --passes=1 --cq-level=10 --rt -o {0} -",
[parameter(Position=0)] [String] $InputFile = ""
)
$ErrorActionPreference = "Stop"
Add-Type @"
using System;
public struct KeyFrameData {
public int start;
public int end;
public decimal startstamp;
public int length;
}
"@
Add-Type @"
using System;
public class ProjectChunkData {
public int index;
public int start;
public int end;
public decimal startstamp;
public bool finished;
public bool muxed;
public Object job;
}
"@
function Write-OurHelp {
Write-Host ""
Write-Host "S. Powershell variant of av1an"
Write-Host "Usage: .\spav1an.ps1 [options] INPUT_FILE"
Write-Host ""
Write-Host "Main options:"
Write-Host " -EncoderOptions <str> The encoding options for the encoder"
Write-Host " -EncoderType 1,2 Select which encoder type we are using: "
Write-Host " 1 = aomenc encoder"
Write-Host " 2 = SVT-AV1 encoder"
Write-Host " -Renderer 1,2 Select which frame renderer we are gonna be"
Write-Host " using for splitting the file:"
Write-Host " 1 = ffmpeg (uses -ss)"
Write-Host " 2 = avisynth"
Write-Host " -MinChunkSec 10 The minimum amount of seconds each chunk"
Write-Host " encode shouldbe at least."
Write-Host " -Threads 0 Total amount of workers to use. Default 0"
Write-Host " will use the total of logical processors"
Write-Host " -Framerate auto Frame rate of the video. Auto will use ffprobe"
Write-Host " to autodect. Otherwise you can specify it using"
Write-Host " rate/scale"
Write-Host " -PrintCommands Print all ffmpeg/ffprobe commands run and arguments"
Write-Host ""
Write-Host "Analyze options:"
Write-Host "The analyze file will be used to generate scenecut changes as well as other"
Write-Host "keyframe data that will be used for splitting the video into chunks."
Write-Host " -AnalyzeVidSize widxhei The width and height of the analyze video"
Write-Host " Smaller size might be more innacurate but"
Write-Host " will be faster to encode"
Write-Host " -AnalyzeVidEnc <str> The encoding options for the analyze video."
Write-Host " By default this is x264 veryfast crf 40"
}
<#
-------------------------------------------------------------------
--------------- Verify all the parameters look okay --------------
-------------------------------------------------------------------
#>
function Assert-OurParameters {
try {
$gobble = Start-ThreadJob -ScriptBlock {}
} catch {
Write-Host "ThreadJob was not found, automatically installing for scope local user"
Install-Module -Name ThreadJob -Scope CurrentUser
Write-Host "Install complete"
}
try {
$gobble = Invoke-NativeCommand -FilePath 'cmd' -ArgumentList ('/c', 'echo test') | Receive-RawPipeline
} catch {
Write-Host "Use-RawPipeline was not found, automatically installing for scope local user"
Install-Module -Name Use-RawPipeline -Scope CurrentUser
Write-Host "Install complete"
}
try {
$gobble = Invoke-NativeCommand -FilePath $FFprobePath -ArgumentList ('-loglevel', 'quiet', '-version') | Receive-RawPipeline
} catch {
Write-Error ("Error, unable to run or find $FFprobePath, make sure path or program exists. Error: " + $_.Exception.Message)
exit 1
}
try {
$gobble = Invoke-NativeCommand -FilePath $FFmpegPath -ArgumentList ('-loglevel', 'quiet', '-version') | Receive-RawPipeline
} catch {
Write-Error ("Error, unable to run or find $FFmpegPath, make sure path or program exists. Error: " + $_.Exception.Message)
exit 1
}
try {
$gobble = Invoke-NativeCommand -FilePath $EncoderPath -ArgumentList ('--help') | Receive-RawPipeline
} catch {
Write-Error ("Error, unable to run or find $EncoderPath, make sure path or program exists. Error: " + $_.Exception.Message)
exit 1
}
$writehelp = $false
if ($inputfile -eq "") {
Write-Host "Error: Inputfile is missing"
Write-Host ""
$writehelp = $true
}
if ($InputFile -notmatch '[^.]+\..+') {
Write-Host "Error: Inputfile must have an extension"
Write-Host ""
$writehelp = $true
}
if ($Framerate -notmatch '\d+/\d+' -and $Framerate -ne 'auto') {
Write-Host "Error: -Framerate has to be in a format of <rate>/<scale> or auto"
Write-Host " Example: -Framerate 24000/1001"
Write-Host " Example: -Framerate 25/1"
Write-Host " Example: -Framerate auto"
Write-Host ""
$writehelp = $true
}
if ($AnalyzeVidSize -notmatch '\d+x\d+') {
Write-Host "Error: -AnalyzeVideoSize has to be in a format of <width>x<height>"
Write-Host " Example: -AnalyzeVideoSize 1280x720"
Write-Host " Example: -AnalyzeVideoSize 848x480"
Write-Host ""
$writehelp = $true
}
if ($EncoderType -ne 1 -and $EncoderType -ne 2) {
Write-Host "Error: Unknown encoder type of $EncoderType."
Write-Host "Only values of 1 and 2 are supported yet."
Write-Host ""
$writehelp = $true
}
if ($Renderer -ne 1 -and $Renderer -ne 2) {
Write-Host "Error: Unknown renderer type of $Renderer."
Write-Host "Only values of 1 and 2 are supported yet."
Write-Host ""
$writehelp = $true
}
if ($writehelp) {
Write-OurHelp
exit
}
}
function Rename-ArrayIdentifiers([String[]] $InputArr, [String] $Identifier1, [String] $ReplaceWith1, [String] $Identifier2, [String] $ReplaceWith2) {
for ($i = 0; $i -le $InputArr.Length; $i++) {
if ($InputArr[$i] -match $Identifier1) {
$InputArr[$i] = $InputArr[$i] -replace $Identifier1, $ReplaceWith1
} elseif ($Identifier2 -and $InputArr[$i] -match $Identifier2) {
$InputArr[$i] = $InputArr[$i] -replace $Identifier2, $ReplaceWith2
}
}
return $InputArr
}
<#
-------------------------------------------------------------------
------- Create our project files --------
-------------------------------------------------------------------
#>
function Write-ProjectFile {
# identifier for text replacement 1
$identifier1 = '2Nhd6VgWZr69B4kzAStydVGCcVugprCpJR'
# identifier for text replacement 2
$identifier2 = 'jh2Acz3nR3XjKrzqPiE3ZBANYYRtEHeqr4'
Write-Host "Project file was not found. Creating a project file."
$analyzefilename = $workproject + '_analyze.mkv'
# Check if framerate is auto and grab it from the source file
if ($Framerate -eq 'auto') {
Write-Host "Framerate is auto, checking framerate from source"
$fpsPropeArgs = "-v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=r_frame_rate $identifier1".Split(' ')
$fpsPropeArgs = Rename-ArrayIdentifiers $fpsPropeArgs $identifier1 $InputFile
if ($PrintCommands) { Write-Host "[Running] $FFprobePath $fpsPropeArgs" }
$Framerate = Invoke-NativeCommand -FilePath $FFprobePath -ArgumentList $fpsPropeArgs | Receive-RawPipeline
Write-Host "Framerate is: $Framerate"
}
# Split the framerate (rate/scale) to their respective values and calculate
# the max keyint from that
$FramerateSplit = $Framerate.Split('/')
$fps = [convert]::ToDouble($FramerateSplit[0]) / [convert]::ToDouble($FramerateSplit[1])
$maxkeyint = [int][Math]::Ceiling($fps * 10)
# Split the video size string of 'widthXheight'
$splitsize = $AnalyzeVidSize.Split('x')
$vfsize = "w=" + $splitsize[0] + ":h=" + $splitsize[1]
# Create our command line
# Reason to use identifier isntead of using filename directly is cause we
# wanna split by space to get array argument list and that might split
# the file pathname.
$AnalyzeVidEnc = $AnalyzeVidEnc -f $identifier1, $vfsize, $maxkeyint, $identifier2
$analyzeffmpegSplitted = Rename-ArrayIdentifiers $AnalyzeVidEnc.Split(' ') $identifier1 $InputFile $identifier2 $analyzefilename
Write-Host "Creating $analyzefilename to generate keyframe split data from"
# Create our analyze file
# Reason we use Invoke-NativeCommand is "ffmpeg.exe" if located in the same
# folder as script will make the Invoke-Expression fail as it requires .\ in
# front of it. Invoke-NativeCommand allows us to specify filepath and it will
# run regardless
if ($PrintCommands) { Write-Host "[Running] $FFmpegPath $analyzeffmpegSplitted" }
# Invoke-NativeCommand -FilePath $FFmpegPath -ArgumentList $analyzeffmpegSplitted | Receive-RawPipeline
Write-Host "Reading keyframe data from $analyzefilename"
$analyzekeyframefilename = $workproject + '_kf.txt'
# Start probing and analyzing our analyze file
$analyzePropeArgs = "-loglevel error -hide_banner -show_entries packet=pts_time,flags -select_streams v:0 -of csv=print_section=0 $identifier1".Split(' ')
$analyzePropeArgs = Rename-ArrayIdentifiers $analyzePropeArgs $identifier1 $analyzefilename
if ($PrintCommands) { Write-Host "[Running] $FFprobePath $analyzePropeArgs" }
$videodata = Invoke-NativeCommand -FilePath $FFprobePath -ArgumentList $analyzePropeArgs | Receive-RawPipeline
# Invoke-Expression ".\$FFprobePath -hide_banner -show_entries packet=pts_time,flags -select_streams v:0 -of csv=print_section=0 $analyzefilename > $analyzekeyframefilename"
# Write-Host ""
$project = New-Object 'System.Collections.Generic.List[KeyFrameData]'
$keyframes = New-Object 'System.Collections.Generic.List[KeyFrameData]'
for ($i = 0; $i -lt $videodata.Length; $i++) {
$split = $videodata[$i].Split(',')
if ($split.Count -gt 1 -and $split[1][0] -eq 'K') {
$keyframes.Add((New-Object KeyFrameData -Property @{
start = $i;
startstamp = [convert]::ToDecimal($split[0]) }))
}
}
# If there are too few frames, the user might as well just encode directly
if ($keyframes.Count -lt 2) {
Write-Host "Too few keyframes found, exiting"
exit
}
$start = New-Object KeyFrameData -Property @{
start = $keyframes[0].start;
startstamp = $keyframes[0].startstamp;
end = 0;
}
for ($i = 1; $i -lt $keyframes.Count; $i++) {
$length = ($keyframes[$i].start - 1) - $start.start
if ($length -gt $maxkeyint) {
if ($keyframes[$i - 1].start - 1 -gt $start.start) {
$start.end = $keyframes[$i - 1].start - 1
$start.length = $start.end - $start.start
$next = $keyframes[$i - 1]
if ($start.end - $start.start -lt $maxkeyint / 2) {
$start.end = $keyframes[$i].start - 1
$start.length = $start.end - $start.start
$next = $keyframes[$i]
}
$project.Add($start)
$start = New-Object KeyFrameData -Property @{
start = $next.start;
startstamp = $next.startstamp;
end = 0;
}
}
}
}
$start.end = $videodata.Length - 1
$start.length = $start.end - $start.start
$project.Add($start)
$projectoutput = '{'
$projectoutput += "`n `"fps`":`"" + $Framerate + "`","
$projectoutput += "`n `"chunks`": ["
$projectoutput += "`n [" + $project[0].start + ", " + $project[0].end + ", " + $project[0].startstamp + ", " + $project[0].length + "]"
for ($i = 1; $i -lt $project.Count; $i++) {
$projectoutput += ",`n [" + $project[$i].start + ", " + $project[$i].end + ", " + $project[$i].startstamp + ", " + $project[$i].length + "]"
}
$projectoutput += "`n ]"
$projectoutput += "`n}"
Set-Content -Path $projectfile $projectoutput
}
<#
-------------------------------------------------------------------
----------------------- Our encoding worker -----------------------
-------------------------------------------------------------------
#>
function Start-OurEncoding {
if ($Threads -lt 1) {
$Threads = (Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors
}
Write-Host "Starting our chunk encoder with maximum $Threads workers"
Write-Host ""
$encodefile = $workproject + '_output'
$projectraw = Get-Content -Path $projectfile | ConvertFrom-Json
$project = New-Object 'System.Collections.Generic.List[ProjectChunkData]'
for ($i = 0; $i -lt $projectraw.Count; $i++) {
$project.Add((New-Object ProjectChunkData -Property @{
index = $i + 1;
start = $projectraw[$i][0];
end = $projectraw[$i][1];
startstamp = $projectraw[$i][2];
finished = $false;
muxed = $false;
job = $null;
}))
}
$chunkformat = ""
$lastChunkFrame = $project.Count.ToString()
for ($x = 0; $x -lt $lastChunkFrame.Length; $x++) {
$chunkformat += '0'
}
$chunkformat = "[{0:$chunkformat}/{1:$chunkformat}]"
$activeworkers = New-Object 'System.Collections.Generic.List[ProjectChunkData]'
for ($i = 0; $i -lt $Threads; $i++) {
$activeworkers.Add([ProjectChunkData]@{})
}
$chunklength = $lastChunkFrame.Length * 2 + 3
for ($i = 0; $i -lt $project.Count) {
$startedchunk = $false
for ($x = 0; $x -lt $activeworkers.Count; $x++) {
if ($null -eq $activeworkers[$x].job) {
$chunkfile = $encodefile + '_' + $project[$i].start + '-' + $project[$i].end + '.ivf'
if ($Renderer -eq 2) {
$trimcontent = $AvisynthScript -f ('"' + $InputFile + '"')
$trimcontent += "`nTrim(" + $project[$i].start + ", " + $project[$i].end + ")"
$chunkavs = $encodefile + '_' + $project[$i].start + '-' + $project[$i].end + '.avs'
Set-Content -Path $chunkavs $trimcontent
$ffmpegreadchunkparameters = @('-hide_banner', '-loglevel', 'warning', '-i', $chunkavs, '-map', '0:0', '-strict', '-1', '-f', 'yuv4mpegpipe', '-')
} elseif ($Renderer -eq 1) {
$sshours = [Math]::Floor($project[$i].startstamp / 3600)
$ssminutes = [Math]::Floor(($project[$i].startstamp % 3600) / 60)
$ssseconds = $project[$i].startstamp % 60
$ssstart = "{0:00}:{1:00}:{2:00.000}" -f $sshours, $ssminutes, $ssseconds
$ffmpegreadchunkparameters = @('-hide_banner', '-loglevel', 'warning', '-ss', $ssstart, '-i', $InputFile, '-map', '0:0', '-vframes', ($project[$i].end - $project[$i].start + 1), '-strict', '-1', '-f', 'yuv4mpegpipe', '-')
}
$identifier1 = '5BdMoB52CQyrNYEof3gARsbLgsTjZCdqF9'
$EncoderParameters = $EncoderOptions -f $identifier1, ($project[$i].end - $project[$i].start + 1)
$EncoderParametersSplitted = $EncoderParameters.Split(' ')
for ($e = 0; $e -le $EncoderParametersSplitted.Length; $e++) {
if ($EncoderParametersSplitted[$e] -match $identifier1) {
$EncoderParametersSplitted[$e] = $EncoderParametersSplitted[$e] -replace $identifier1, $chunkfile
}
}
"Encoding chunk " + ($chunkformat -f $project[$i].index, $project.Count) + " " + $project[$i].start + ' - ' + $project[$i].end + ' (' + ($project[$i].end - $project[$i].start + 1) + ')'
$job = Start-ThreadJob -ArgumentList @($FFmpegPath, $ffmpegreadchunkparameters, $EncoderPath, $EncoderParametersSplitted, $i) -ScriptBlock {
$lecommand = $args[0] + ' ' + $args[1] + ' | ' + $args[2] + ' ' + $args[3]
$lecommand
cmd.exe /C $lecommand
}
$project[$i].job = $job
$activeworkers[$x] = $project[$i]
$i++
$startedchunk = $true
break
}
}
if ($startedchunk -eq $false) {
$chunkfinished = $false
while ($chunkfinished -eq $false) {
$longest = 15 + $chunklength
for ($x = 0; $x -lt $activeworkers.Count; $x++) {
if ($null -ne $activeworkers[$x].job) {
$currentprogress = ($activeworkers[$x].job.Error | Select-Object -Last 1)
if ($null -ne $currentprogress) {
$longest = [Math]::Max($longest, $currentprogress.ToString().Length + 15 + $chunklength)
}
($chunkformat -f $activeworkers[$x].index, $project.Count) + ' ' + $activeworkers[$x].job.State + ' ' + $currentprogress
if ($activeworkers[$x].job.State -eq 'Completed') {
# $activeworkers[$x].job
$activeworkers[$x].job = $null
$activeworkers[$x].finished = $true
$chunkfinished = $true
}
}
}
Start-Sleep 1
[Console]::SetCursorPosition(0, $Host.UI.RawUI.CursorPosition.Y - $activeworkers.Count)
for ($x = 0; $x -lt $activeworkers.Count; $x++) {
[Console]::WriteLine(("{0,-" + ($longest) + "}") -f " ")
}
[Console]::SetCursorPosition(0, $Host.UI.RawUI.CursorPosition.Y - $activeworkers.Count)
}
}
}
Write-Host "Waiting for jobs to finish"
$jobsleft = 1
while ($jobsleft -gt 0) {
$longest = 15 + $chunklength
$jobsleft = 0
for ($x = 0; $x -lt $activeworkers.Count; $x++) {
if ($null -ne $activeworkers[$x].job) {
$jobsleft++
$currentprogress = ($activeworkers[$x].job.Error | Select-Object -Last 1)
if ($null -ne $currentprogress) {
$longest = [Math]::Max($longest, $currentprogress.ToString().Length + 15 + $chunklength)
}
($chunkformat -f $activeworkers[$x].index, $project.Count) + ' ' + $activeworkers[$x].job.State + ' ' + $currentprogress
if ($activeworkers[$x].job.State -eq 'Completed') {
$activeworkers[$x].job = $null
$activeworkers[$x].finished = $true
}
}
}
Start-Sleep 1
[Console]::SetCursorPosition(0, $Host.UI.RawUI.CursorPosition.Y - $jobsleft)
for ($x = 0; $x -lt $jobsleft; $x++) {
[Console]::WriteLine(("{0,-" + ($longest) + "}") -f " ")
}
[Console]::SetCursorPosition(0, $Host.UI.RawUI.CursorPosition.Y - $jobsleft)
}
Write-Host "Finished"
}
try {
Assert-OurParameters
$workproject = [io.path]::GetFileNameWithoutExtension($InputFile)
$projectfile = $workproject + '_spav1an.json'
if (!(Test-Path $projectfile)) {
Write-ProjectFile
}
}
catch {
Write-Host ""
Write-Host "FATAL EXCEPTION OCCURED:"
Write-Host ""
Write-Host $_
Write-Host $_.ScriptStackTrace
}
# Start-OurEncoding
# .\ffmpeg.exe -hide_banner -i gi_joe_720p_275.webm -map 0:0 -vf "zscale=w=1280:h=720:f=spline36" -pix_fmt yuv420p -strict -1 -f yuv4mpegpipe - | .\aomenc-3.0.0-335.exe --cpu-used=6 --cq-level=20 --bit-depth=8 -o gi_joe_test.webm -
# ./ffprobe.exe -hide_banner -show_entries packet=pts_time,flags -select_streams v:0 -of csv=print_section=0 gi_joe_720p_275.webm > test2.txt
# .\ffmpeg.exe -i gi_joe_720p_275.webm -vf "zscale=w=848:h=480:f=spline36" -map 0:0 -vcodec libx264 -preset veryfast -crf 30 -x264-params keyint=240:min-keyint=5:scenecut=10 -y -pix_fmt yuv420p gi_joe_720p_275_x264.mp4