ffmpeg工具把m4s合并为mp4 powershell脚本

需要修改 $RootDir 和 $FfmpegBin指定到相应的目录,然后powershell下执行脚本。

param(
    [string]$RootDir = 'C:\Users\guest\Videos\bilibili',
    [string]$FfmpegBin = 'C:\ffmpeg-n8.1.1-11-ge4c7fbf6c0-win64-lgpl-8.1\bin',
    [switch]$Force,
    [switch]$WhatIf
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$ffmpeg = Join-Path $FfmpegBin 'ffmpeg.exe'
$ffprobe = Join-Path $FfmpegBin 'ffprobe.exe'

if (-not (Test-Path $RootDir)) {
    throw "Root directory does not exist: $RootDir"
}
if (-not (Test-Path $ffmpeg)) {
    throw "ffmpeg not found: $ffmpeg"
}
if (-not (Test-Path $ffprobe)) {
    throw "ffprobe not found: $ffprobe"
}

function Get-SafeFileName {
    param([string]$Name)

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return 'output'
    }

    $invalid = [System.IO.Path]::GetInvalidFileNameChars()
    $safe = -join ($Name.ToCharArray() | ForEach-Object {
        if ($invalid -contains $_) { '_' } else { $_ }
    })

    $safe = $safe.Trim()
    if ([string]::IsNullOrWhiteSpace($safe)) {
        return 'output'
    }
    return $safe
}

function Needs-PrefixStrip {
    param([string]$FilePath)

    $fs = [System.IO.File]::OpenRead($FilePath)
    try {
        if ($fs.Length -lt 14) {
            return $false
        }

        $buf = New-Object byte[] 14
        $read = $fs.Read($buf, 0, 14)
        if ($read -lt 14) {
            return $false
        }

        # Some bilibili m4s files prepend 9 ASCII '0' bytes before normal fMP4 header.
        for ($i = 0; $i -lt 9; $i++) {
            if ($buf[$i] -ne 0x30) {
                return $false
            }
        }

        return ($buf[13] -eq 0x66) # 'f' in 'ftyp'
    }
    finally {
        $fs.Dispose()
    }
}

function Prepare-M4sFile {
    param(
        [string]$InputPath,
        [string]$WorkDir
    )

    if (-not (Needs-PrefixStrip -FilePath $InputPath)) {
        return $InputPath
    }

    $tmpName = [System.IO.Path]::GetFileNameWithoutExtension($InputPath) + '.clean.m4s'
    $tmpPath = Join-Path $WorkDir $tmpName

    $bytes = [System.IO.File]::ReadAllBytes($InputPath)
    if ($bytes.Length -le 9) {
        throw "File too short to strip prefix: $InputPath"
    }

    [System.IO.File]::WriteAllBytes($tmpPath, $bytes[9..($bytes.Length - 1)])
    return $tmpPath
}

function Get-CodecType {
    param([string]$FilePath)

    $result = & $ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=nokey=1:noprint_wrappers=1 $FilePath
    if ($LASTEXITCODE -eq 0 -and ($result -join '') -match 'video') {
        return 'video'
    }

    $result = & $ffprobe -v error -select_streams a:0 -show_entries stream=codec_type -of default=nokey=1:noprint_wrappers=1 $FilePath
    if ($LASTEXITCODE -eq 0 -and ($result -join '') -match 'audio') {
        return 'audio'
    }

    return 'unknown'
}

function Get-OutputBaseName {
    param([string]$FolderPath)

    $infoPath = Join-Path $FolderPath 'videoInfo.json'
    if (Test-Path $infoPath) {
        try {
            $raw = Get-Content -Path $infoPath -Raw -Encoding UTF8
            $obj = $raw | ConvertFrom-Json
            if (-not [string]::IsNullOrWhiteSpace($obj.tabName)) {
                return (Get-SafeFileName -Name $obj.tabName)
            }
            if (-not [string]::IsNullOrWhiteSpace($obj.title)) {
                return (Get-SafeFileName -Name $obj.title)
            }
        }
        catch {
            # Ignore parse errors and fall back to folder name.
        }
    }

    return (Get-SafeFileName -Name ([System.IO.Path]::GetFileName($FolderPath)))
}

$dirs = Get-ChildItem -Path $RootDir -Directory | Sort-Object Name
if ($dirs.Count -eq 0) {
    Write-Host "No subdirectories found under: $RootDir"
    exit 0
}

$ok = 0
$skip = 0
$fail = 0

foreach ($dir in $dirs) {
    try {
        Write-Host "`n==> Processing: $($dir.FullName)"

        $m4s = Get-ChildItem -Path $dir.FullName -File -Filter '*.m4s' |
            Where-Object { $_.Name -notlike '*.clean.m4s' } |
            Sort-Object Length -Descending

        if ($m4s.Count -lt 2) {
            Write-Host "  Skip: less than 2 m4s files"
            $skip++
            continue
        }

        $tempFiles = New-Object System.Collections.Generic.List[string]
        $prepared = @()

        foreach ($f in $m4s) {
            $p = Prepare-M4sFile -InputPath $f.FullName -WorkDir $dir.FullName
            if ($p -ne $f.FullName) {
                $tempFiles.Add($p)
            }
            $prepared += [PSCustomObject]@{
                Source = $f.FullName
                Path   = $p
                Kind   = (Get-CodecType -FilePath $p)
            }
        }

        $video = $prepared | Where-Object { $_.Kind -eq 'video' } | Select-Object -First 1
        $audio = $prepared | Where-Object { $_.Kind -eq 'audio' } | Select-Object -First 1

        if (-not $video -or -not $audio) {
            Write-Host "  Skip: cannot identify both video and audio streams"
            $skip++
            foreach ($tmp in $tempFiles) {
                if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
            }
            continue
        }

        $base = Get-OutputBaseName -FolderPath $dir.FullName
        $outPath = Join-Path $dir.FullName ($base + '.mp4')

        if ((Test-Path $outPath) -and (-not $Force)) {
            Write-Host "  Skip: output exists ($outPath). Use -Force to overwrite."
            $skip++
            foreach ($tmp in $tempFiles) {
                if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
            }
            continue
        }

        if ($WhatIf) {
            Write-Host "  WhatIf: would merge"
            Write-Host "    video: $($video.Path)"
            Write-Host "    audio: $($audio.Path)"
            Write-Host "    output: $outPath"
            $ok++
            foreach ($tmp in $tempFiles) {
                if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
            }
            continue
        }

        & $ffmpeg -y -i $video.Path -i $audio.Path -c copy -map 0:v:0 -map 1:a:0 $outPath | Out-Null
        if ($LASTEXITCODE -ne 0) {
            throw "ffmpeg failed with code $LASTEXITCODE"
        }

        foreach ($tmp in $tempFiles) {
            if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
        }

        Write-Host "  OK: $outPath"
        $ok++
    }
    catch {
        Write-Host "  Fail: $($_.Exception.Message)"
        $fail++
    }
}

Write-Host "`nDone. Success: $ok, Skipped: $skip, Failed: $fail"

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值