iPhone/iPadで録画した動画の位置情報:位置情報を保持して圧縮
2018/04/07
iPhoneで撮影した動画には位置情報(ジオタグ)が記録されています。でも動画を圧縮(再エンコード)すると位置情報は消えてしまいます。
前回はiOS8まででうまくいく方法を見つけました。今回はiOS9移行でも使える方法を見つけ(というか作りだし)ました。
目次 [隠す]
iOSの位置情報の仕組み
前回は位置情報記録の仕組みについて調査して、[mov→mov]の変換であれば位置情報が維持できましたが、iOS9以降で撮影した動画では位置情報の記録が微妙に変わってしまいました。
前回の話↓

2種類の記録
GOMプレーヤーで動画のメタデータを見てみると2種類のデータ名で記録されていることが分かりました。
com.apple.quicktime .location.ISO6709 |
ゥxyz | ||
---|---|---|---|
記録 | ~iOS 8 | ○ | ○ |
iOS 9~ | ○ | × | |
情報の優先順位 | 高 | 低 | |
再エンコード(mov→mp4) での位置情報の維持 |
× | × | |
再エンコード(mov→mov) での位置情報の維持 |
× | ○ |
iOS8までの動画であれば mov→mov で再エンコードすれば「ゥxyz」の方が残ってくれたのですが、iOS9以降ではそもそもそちらが記録されていません。なので、なんとかして「com.apple.quicktime.location.ISO6709」の方を残す方法を検討します。こちらの方が情報の優先順位(両方記録されていた場合にどちらの情報を表示するか)が高いので、こっちの方が正当な方法ですしね。
必要な部分を手動でコピーする
残念ながらffmpegでは位置情報が残りません。それに他のツールでもおそらく同じでしょう。仕方がないので、位置情報を含むメタデータ全体を手動でコピーする方法を考えます。
movのファイル構造
movやそれを元にしたmp4では、「Atom」と呼ばれるデータ構造になっています。
Atomでは先頭から[データの長さ4バイト][データの名前4バイト][データ]の順で並んでいます。「データの長さ」は先頭の8バイトを含むバイト数が、ビッグエンディアンで記録されています。データの名前はASCIIのアルファベット4文字で「ftyp」や「moov」などです。Atomはデータを入れ子にすることができます。
「meta」が消える
ffmpegでmov→movで再エンコードすると、Atomの「meta」(moovの中に含まれる)要素が消えてしまいます。GPSの情報が記録されている「com.apple.quicktime.location.ISO6709」はmetaの中にあるので、一緒に消えてしまっているわけですね。そこでこんな手順を踏むことにしました。
- movファイルA(元)をffmpegで好きなサイズに圧縮して、movファイルBを得る。
- movファイルAのmeta要素の中身を抜き出す。
- movファイルBのAtom構造に、Aのmeta要素を適切に入れる。
Atomのデータ構造自体は単純なので問題なくできそうですが、一つだけ注意点があります。
moovより後にデータがあるとうまくいかない
再エンコード後のファイルBのAtom構造が、moovよりも後にデータを含むようになっているとどうやらうまくいかないようです。Atom構造の編集ミスかと思って何度かトライしたんですが、うまくいきません。
今回はエンコードすること前提なので、エンコード時にmoovが最後に来るようにしておけば問題ありません。ffmpegの場合「-movflags faststart」を付けていなければOKです。
重大な問題点
今回作ったプログラムで確かにGPS情報はコピーされます。GOMプレーヤーでメタデータをみるとちゃんと記録されていますし、Dropbox経由でiPhoneにファイルをダウンロードしたらちゃんとGPSを元にした住所が表示されました。
でも私には意味がありませんでした。なぜなら私の目的は「iTunesの写真同期でiPhoneに入れた過去の動画ファイルに、位置情報を付加したい」だったからです。残念ながらiTunesの写真同期で入れた動画の位置情報は読み取ってくれませんでした。これでは意味がない!
iTunes写真同期に最適な解像度を調査したときもそうでしたが、写真同期機能はファイルをそのままコピーしてくれるわけではないんですね……。

ソースコード
iTunes写真同期ではダメでしたが、GPS情報のコピー自体はうまくいったのでご紹介しておきます。
いつも通りAutoHotkeyで書きました。CopyMovMetaDataにファイルA,Bのファイルパスを渡すと、Aのmetaを丸ごとBに埋め込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | CopyMovMetaData(pSourceFilePath, pDestFilePath) { global gMetaData, gMetaLength ; metaを読み込み 読み込んだデータはグローバル変数に格納される sourceFile := FileOpen(pSourceFilePath, "r" ) ReadAtom(sourceFile, 0) sourceFile. Close () ; metaを書き込み destFile := FileOpen(pDestFilePath, "rw" ) UpdateAtom(destFile, 0) destFile. Close () } ReadAtom( ByRef file, pOffset) { global gMetaData, gMetaLength pos := file.Position file.Seek(pOffset, 0) pos2 := file.Position VarSetCapacity (buff, 16) Loop { dataLen := file.RawRead(buff, 4) if (dataLen <= 0) break atomLength := NumGetOrder(&buff, 0, 4, true ) dataLen := file.RawRead(buff, 4) atomName := StrGet(&buff, 4, "CP0" ) ; Atomがmetaなら中のデータを gMetaData に格納 if atomName in meta { VarSetCapacity (gMetaData, atomLength + 16) file.Seek(-8, 1) file.RawRead(gMetaData, atomLength) gMetaLength := atomLength break } ; ポジションを保存して中身を探索(再帰) posBuff := file.Position if atomName in moov ReadAtom(file, file.Position) file.Position := posBuff ; Atomのサイズだけ進める file.Seek(atomLength - 8, 1) if atomName in data file.Seek(8, 1) } } ; ファイルがすでにmetaを含んでいたら true を返す HasMetaInMoov( ByRef file) { filePosition := file.Position VarSetCapacity (buff, 16) Loop { dataLen := file.RawRead(buff, 4) if (dataLen <= 0) break atomLength := NumGetOrder(&buff, 0, 4, true ) dataLen := file.RawRead(buff, 4) atomName := StrGet(&buff, 4, "CP0" ) ; metaを発見 if atomName in meta { file.Position := filePosition return true } ; Atomのサイズだけ進める file.Seek(atomLength - 8, 1) if(file.Position >= file.Length) break } file.Position := filePosition return false } UpdateAtom( ByRef file, pOffset) { global gMetaData, gMetaLength file.Seek(pOffset, 0) VarSetCapacity (buff, 16) Loop { dataLen := file.RawRead(buff, 4) if (dataLen <= 0) break atomLength := NumGetOrder(&buff, 0, 4, true ) dataLen := file.RawRead(buff, 4) atomName := StrGet(&buff, 4, "CP0" ) if atomName in moov { ; moovの先頭(長さ含む)に移動 file.Seek(-8, 1) if (file.Position + atomLength < file.Length) { MsgBox , 0x30, mov, moovの後ろにもデータがあります。 return } else if (file.Position + atomLength > file.Length) { MsgBox , 0x30, mov, moovのサイズがファイルサイズをはみ出しています。 return } ; すでにmetaがあるかチェック file.Seek(8, 1) if (HasMetaInMoov(file)) { MsgBox , 0x30, mov, すでにmetaがあります。 return } file.Seek(-8, 1) ; 箱を用意して新しい長さを入れる VarSetCapacity (buff2, 16) NumPutOrder(atomLength + gMetaLength, &buff2) ; 新しい長さに変更 file.RawWrite(&buff2, 4) ; アップデートした4バイト戻して file.Seek(-4, 1) ; moovの最後まで移動 file.Seek(atomLength, 1) ; metaデータを追記 file.RawWrite(&gMetaData, gMetaLength) return } ; Atomのサイズだけ進める file.Seek(atomLength - 8, 1) } } ; 連続したバイト列に入っているデータを数値として取得 ; 例:000213FF → 136191 ; 例:FF130200 → 136191 (little endian) NumGetOrder(pAddr, pOffset, pLength, pBigEndian= true ) { global ret := 0 Loop , %pLength% { ret *= 256 if (pBigEndian) ret += *(pAddr+pOffset+ A_Index -1) else ret += *(pAddr+pOffset+pLength- A_Index ) } return ret } ; 連続したバイト列に入っているデータを数値として取得 ; 例:000213FF → 136191 ; 例:FF130200 → 136191 (little endian) NumPutOrder(pNum, pAddr, pOffset=0, pLength=4, pBigEndian= true ) { global Loop , %pLength% { buff_num1 := pNum // 256**(pLength- A_Index ) buff_num2 := Mod (buff_num1, 256) if (pBigEndian) NumPut (buff_num2, pAddr+0, pOffset+ A_Index -1, "UChar" ) else NumPut (buff_num2, pAddr+0, pOffset+pLength- A_Index , "UChar" ) } } |
まとめ
iTunes写真同期の挙動に翻弄されてばかりですね。同期がおかしくなったときはこちら↓
