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

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

bash 复制代码
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"
相关推荐
luoyayun3611 天前
Qt + FFmpeg 实战:实现音频格式转换功能
qt·ffmpeg·音频格式转换
都在酒里2 天前
【极致低延时】香橙派部署 MediaMTX 实现 WebRTC 推流,延时仅 500-800ms,比局域网 ffmpeg 拉流快近 10 倍!(附踩坑全记录)
linux·arm开发·ffmpeg·webrtc·orangepi·嵌入式软件
Empty-Filled2 天前
用 Kap + FFmpeg 把录屏转成小体积 GIF:产品操作演示图制作实践
ffmpeg·kap
矜辰所致3 天前
嵌入式语音开发应用基础说明
ffmpeg·ai 语音·嵌入式语音·语音播放·语音采样
luoyayun3613 天前
Qt + FFmpeg 实战:音频静音段检测
qt·ffmpeg·音视频·静音段检测
小鹿研究点东西5 天前
直播带货长视频AI自动剪辑开播:一场直播如何反复利用?
ffmpeg·自动化·音视频·语音识别
luoyayun3615 天前
Qt + FFmpeg 实战:获取音视频文件基础属性、流信息和元数据
qt·ffmpeg·音视频·元数据·获取音视频文件属性
Rudon滨海渔村5 天前
ffmpeg裁剪视频黑屏、不准时等处理方式 - ffmpeg基本操作
ffmpeg·音视频
The Sheep 20236 天前
ffmpeg速成
ffmpeg