iPhone/iPadで録画した動画の位置情報:位置情報を保持して圧縮

      2018/04/07

iPhoneで撮影した動画には位置情報(ジオタグ)が記録されています。でも動画を圧縮(再エンコード)すると位置情報は消えてしまいます。

前回はiOS8まででうまくいく方法を見つけました。今回はiOS9移行でも使える方法を見つけ(というか作りだし)ました。

SPONSORED LINK

iOSの位置情報の仕組み

前回は位置情報記録の仕組みについて調査して、[mov→mov]の変換であれば位置情報が維持できましたが、iOS9以降で撮影した動画では位置情報の記録が微妙に変わってしまいました。

前回の話↓

iPhone/iPadで録画した動画の位置情報:記録方法や書き換え方
iPhoneで撮影した動画には位置情報(ジオタグ)が記録されています。その記録内容や方法について調べました。 動画の位置情報 iPhone/iPadで撮影した動画には位... 続きを読む

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の中にあるので、一緒に消えてしまっているわけですね。そこでこんな手順を踏むことにしました。

  1. movファイルA(元)をffmpegで好きなサイズに圧縮して、movファイルBを得る。
  2. movファイルAのmeta要素の中身を抜き出す。
  3. movファイルBのAtom構造に、Aのmeta要素を適切に入れる。

Atomのデータ構造自体は単純なので問題なくできそうですが、一つだけ注意点があります。

moovより後にデータがあるとうまくいかない

再エンコード後のファイルBのAtom構造が、moovよりも後にデータを含むようになっているとどうやらうまくいかないようです。Atom構造の編集ミスかと思って何度かトライしたんですが、うまくいきません。

今回はエンコードすること前提なので、エンコード時にmoovが最後に来るようにしておけば問題ありません。ffmpegの場合「-movflags faststart」を付けていなければOKです。

SPONSORED LINK

重大な問題点

今回作ったプログラムで確かにGPS情報はコピーされます。GOMプレーヤーでメタデータをみるとちゃんと記録されていますし、Dropbox経由でiPhoneにファイルをダウンロードしたらちゃんとGPSを元にした住所が表示されました。

でも私には意味がありませんでした。なぜなら私の目的は「iTunesの写真同期でiPhoneに入れた過去の動画ファイルに、位置情報を付加したい」だったからです。残念ながらiTunesの写真同期で入れた動画の位置情報は読み取ってくれませんでした。これでは意味がない!

iTunes写真同期に最適な解像度を調査したときもそうでしたが、写真同期機能はファイルをそのままコピーしてくれるわけではないんですね……。

iPhone/iPadへの写真同期に最適な解像度・品質・ファイルサイズをキッチリ調査した
iPhoneやiPadに写真を同期する場合、写真のサイズはどんなサイズで同期するのが最適なのかキッチリ調査しました。 同期する際に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写真同期の挙動に翻弄されてばかりですね。同期がおかしくなったときはこちら↓

iPhoneの写真同期がおかしい・空き容量が増えない…設定をリセットしよう!
iPhone/iPadの写真同期がおかしい、同期した写真の表示が壊れた、同期を解除したのに空き容量が増えない、などの問題の解決方法です。iTunesに頼らずに同期設定をすっ... 続きを読む

itjo レスポンシブ 本文下

 - Apple
 - , , , , , , ,

S