2020-09-15

設定MP4封面(縮圖)

把這個ps1 放在相同主檔名的 mp4  jpg旁邊
按右鍵 → 用PowerShell 執行

只有moov在檔案尾端的mp4,才會處理
只有寫入幾百KB的資料,很快

程式碼流程

1.理解MP4的box樹狀結構
    請去 google mp4 結構
    封面是放在 moov\udta\meta\ilst\covr 裡面

2.確認這個MP4是否正確
    讀取MP4、尋找 ftyp moov mdat

3.試著去尋找 moov\udta\meta\ilst\covr
    如果有哪一層box不存在,等一下會去建立它

    根據尋找結果,得知「插入點」、「備份點」
        通常,插入點在前、備份點在後
        插入點:從這個位置開始,將會寫入「新建立的box」和「圖片」
        備份點:從這個位置開始,將會把內容備份到buffer
                因為,寫入「新建立的box」和「圖片」內容太多,可能會覆蓋到這裡的資料

4.把「新建立的box」和「圖片」,寫到buffer
    把「備份點」之後的資料,寫到buffer
    在「插入點」,把buffer裡的資料寫入MP4

5.如果「加入的圖片」比「原本的圖片」小
    MP4的檔案大小,會比設定封面之前還要小
    要用 $fs.SetLength 設定MP4正確的檔案大小
    這樣可以:清除最後面垃圾資料、避免MP4在讀取時解讀失敗

6.修改「原本已存在的box」的box size


程式碼

function FindBox_1st($fs, $start, $end, $boxPath){
    $buffer = $script:buffer
    $fs.position = $start #從 $start 開始找
$arrBoxName = $boxPath -split '\\' $index = 0 $targetBox = $arrBoxName[0]
$result = @()
while($fs.position -lt $end){ if ($fs.position + 8 -gt $end){throw 'no enough data'}
$boxBegin = $boxSizePos = $fs.position # 讀取 8 byte (內含boxSize 4byte 、 boxName 4byte ) [void]$fs.read($buffer, 0, 8)
#MP4是以 big endian 存放 boxSize 資料 #然而,.net frameWork 在處理資料時,認為資料是以 little endian 存放 $boxSize = [System.BitConverter]::ToUInt32($buffer[3..0], 0) $boxEnd = $boxBegin + $boxSize $boxName = [char[]]$buffer[4..7] -join ''
#$boxSize 只有 4byte,可以表示的大小為 0 到 (4GB -1) #如果 $boxSize 等於 1,表示接下來有 8 byte 存放真正的boxSize #即 boxSize 4byte(等於 1)、boxName 4byte、boxSize 8byte if ($boxSize -eq 1){#largeSize if ($fs.position + 8 -gt $end){throw 'no enough data'} $boxSizePos = $fs.position [void]$fs.read($buffer, 0, 8)
$boxSize = [System.BitConverter]::ToUInt64($buffer[7..0], 0) $boxEnd = $boxBegin + $boxSize
}elseif ($boxSize -lt 8 ){#boxSize等於 0 有別的意思,這裡沒有處理 throw 'mp4 error' }#end of if #=================================== if ($boxEnd -gt $end){throw "$boxName Box incomplete"}
if ($targetBox -eq '*'){#第一層全部列出 $h = @{boxName = $boxName; boxSizePos = $boxSizePos; boxSize = $boxSize; boxBegin = $boxBegin; boxEnd = $boxEnd} $result += $h $fs.position = $boxEnd
}elseif ($targetBox -eq $boxName){#找到了 $h = @{boxName = $boxName; boxSizePos = $boxSizePos; boxSize = $boxSize; boxBegin = $boxBegin; boxEnd = $boxEnd} $result += $h
$index++ if ($index -lt $arrBoxName.length){#還有其他要找的 $targetBox = $arrBoxName[$index] # meta box 是 fullBox,多了 4byte(flag version) if ($boxName -eq 'meta') {$fs.position += 4} }else{#找完了 return $result }
}else{#沒找到 $fs.position = $boxEnd } }#end of while
return ,$result }
function CheckMp4($fs){#確認這個MP4是否正確 #尋找MP4 第一層的所有box $result = FindBox_1st $fs 0 $fs.length '*'
$boxBegin = @{} # 建立一個空的 hashtable 來存放資料 foreach ($h in $result){# $result裡面有許多 hashtable $boxBegin[$h.boxName] = $h.boxBegin }
#確認 ftyp moov mdat 這些box 是否存在 if ($boxBegin.ftyp -ne 0 -or $boxBegin.moov -eq $null -or $boxBegin.mdat -eq $null){ throw 'This is not a MP4 file' }
#不處理「moov在檔案開始」的mp4 if ($boxBegin.moov -lt $boxBegin.mdat){ throw "can't process mp4 that moov in the beginning of file" } }

function MakeAddBox($fs, [string]$boxPath, $result, $srcFile){ $arrBoxName = $boxPath -split '\\' #缺少多少box?(有多少需要被建立?) $n = $arrBoxName.count - $result.count if ($n -eq 0) {$n = 1} #沒有缺少box。但是,最後一個box總是需要被重新建立
#讀取圖片資料 [byte[]]$srcData = [IO.File]::ReadAllBytes($srcFile)
if (!$srcData.Count){return} #============================================= #covr box裡面,其實有一個 data box $arrBoxName += 'data' $n += 1
[byte[]]$newBox = @() [UInt32]$TotalLength = $srcData.Length #初始值是圖片大小
#moov\udta\meta\ilst\covr\data #如果有缺少box,從最後一個box開始建立 -1 .. -$n | %{ $boxName = $arrBoxName[$_]
#$after8 表示 (boxSize boxName)後面的資料 [byte[]]$after8 = @()
if ($boxName -eq 'data'){ $after8 = [byte[]](0,0,0,13,0,0,0,0) }
if ($boxName -eq 'meta'){ $after8 = [byte[]](0,0,0,0) #fullBox(flag version) # meta box 裡面有 hdlr box。這是hdlr box的完整內容 $hdlr = [byte[]](0,0,0,33 + 'hdlr'[0..3] + ,0*8 + 'mdirappl'[0..7] + ,0*9) $after8 += $hdlr } $TotalLength += 8 + $after8.Length $arrBytes = [System.BitConverter]::GetBytes($TotalLength) #MP4是以big endian存放資料。而.net framework是以 little endian $newBox = $arrBytes[3..0] + $boxName[0..3] + $after8 + $newBox
}#end of foreach
#傳回資料。不使用 return $newBox + $srcData的形式 $newBox $srcData }

function insertBox($fs, $BoxPath, $srcFile){ $buffer = $script:buffer
#試著去尋找 moov\udta\meta\ilst\covr $result = FindBox_1st $fs 0 $fs.length $BoxPath [byte[]]$AddBox = @() $arrBoxName = $boxPath -split '\\' if ($srcFile.length -gt 0){#make addbox #根據 $result 建立「缺少的box」 #最後一個box covr 不管有沒有找到,都會建立 #AddBox 包含 「新建立的box」和「圖片」 $AddBox = MakeAddBox $fs $BoxPath $result $srcFile }else{#clear box #no need to clear box #找到的box太少,不用去清除box if ($result.Count -lt $arrBoxName.Count){return} }
#============$insertPos $backupPos=========== if ($result.count -lt $arrBoxName.count) { # 有些box沒找到 $insertPos = $backupPos = $result[-1].boxEnd
}else{ # 所有box 都找到了。重建 or 清除 covr box $insertPos = $result[-1].boxBegin $backupPos = $result[-1].boxEnd #不備份 covr 的資料 }
#======= 把「新建立的box」和「圖片」,寫到buffer ========= $bufferPos = 0 if ($AddBox.count){#如果有東西要寫入 if ($AddBox.count -gt $buffer.length){ throw "bufferSize $($buffer.length/1MB)MB is too small" } [array]::copy($AddBox, 0, $buffer, $bufferPos, $AddBox.Count) $bufferPos += $AddBox.Count #下次要寫入buffer時,會從 $bufferPos開始 } #=========== 把「備份點」之後的資料,寫到buffer =============== $n = 0
#如果「加入的圖片」和「原本的圖片」檔案大小不一樣 #以致於 $insertPos + $AddBox.Count 不等於 $backupPos if ($insertPos + $AddBox.Count -ne $backupPos){ $n = $fs.length - $backupPos }
#如果「加入的圖片」和「原本的圖片」檔案大小是一樣的 #$n 會是 0,就不用把「備份點」之後的資料,寫到buffer if ($n -gt 0){ if ($n -gt $buffer.length - $bufferPos){ throw "bufferSize $($buffer.length/1MB)MB is too small" } $fs.position = $backupPos $bufferPos += $fs.read($buffer, $bufferPos, $n) }
#======= 在「插入點」,把buffer裡的資料寫入MP4 =========== $fs.position = $insertPos $fs.write($buffer, 0, $bufferPos)
#============ 如果「加入的圖片」比「原本的圖片」小============= $editSize = $AddBox.Count - ($backupPos - $insertPos) if ($editSize -lt 0){# 設定MP4正確的檔案大小 $fs.SetLength($fs.length + $editSize) }
#==========修改「原本已存在的box」的box size============ if ($result.count -eq $arrBoxName.count){# 所有box都有找到 #最後一個box不用修改。因為它總是被重新建立 $n = $result.count - 1
}else{ # 有些box沒找到 $n = $result.count } for ($i = 0 ; $i -lt $n ; $i++) { if ($result[$i].boxSize -is 'UInt32'){ [UInt32]$boxSize = $result[$i].boxSize + $editSize $arrBytes = [System.BitConverter]::GetBytes($boxSize) $arrBytes = $arrBytes[3..0] #MP4 是以 big endian 存放資料 #但是,.net framework是以 little endian 存放資料 }else{ [UInt64]$boxSize = $result[$i].boxSize + $editSize $arrBytes = [System.BitConverter]::GetBytes($boxSize) $arrBytes = $arrBytes[7..0] } $fs.position = $result[$i].boxSizePos $fs.write($arrBytes, 0, $arrBytes.count) } }
function addMetaData($mp4, $srcFile){ trap{$_; $fs.close(); read-host; return}
$fs = $mp4.Open('open', 'readWrite', 'read')
#確認這個MP4是否正確 CheckMp4 $fs
if ($srcFile.length -gt 3MB){throw 'jpg size is too big'} $boxPath = 'moov\udta\meta\ilst\covr' insertBox $fs $boxPath $srcFile
$fs.close() }
#===================================================== $bufferSize = 20MB $buffer = new-object byte[]($bufferSize)
#切換工作目錄到 ps1 所在地 #cd -literal $PSScriptRoot cd -literal (Split-Path $MyInvocation.MyCommand.Path)

#列出 mp4 檔案 #dir *.mp4 -file |%{ dir *.mp4 |?{! $_.PSIsContainer}|%{ $mp4 = $_ $jpgName = $mp4.BaseName + '.jpg'
#有沒有相同主檔名的jpg if (Test-Path -literal $jpgName -PathType Leaf){ $jpg = gi -literal $jpgName addMetaData $mp4 $jpg } }#end of foreach
$buffer = $null

沒有留言:

張貼留言