JavaScriptでMP4の動画からAACを抽出してみた

JavaScript Advent Calendar 2011 (オレ標準コース) : ATNDの11日目の記事です。

HTML5ではArrayBuffer、TypedArray、BlobBuilderなどのバイナリを扱うのに適したAPIがあります。というわけで、試しにMP4の動画からAACを抽出してみました。ソースコードはgithubを参照してください。


MP4ファイルからAACを抽出する手順は以下のようになります。

1.MP4ファイルをパースする
2.パースした情報を元に、音声部分を連結する


じゃ、まずMP4ファイルをパースしてみます。

MP4や、その派生である携帯電話向けの3GPP、3GPP2などのファイルフォーマットはボックス(あるいはその基になったQuickTimeでの用語のAtom)と呼ばれるデータブロックで構成されます。ボックスによってはその内部にさらにボックスが入れ子になるツリー構造になっています。

各ボックスはその先頭8バイト(オクテット)がボックスを識別するためのヘッダで、最初の4バイト(オクテット)がボックスのサイズ、続く4バイトがそのタイプです

ウノウラボ by Zynga Japan: MP4/3GPP/3GPP2ファイルフォーマットの基礎知識

そういうわけなので、ハッシュ(オブジェクト)に関数をぶちこんでぐるぐる回せばツリー構造が取り出せます(今回の目的はAAC抽出するだけなので、関係ある部分だけ実装すればOK)。

Mp4.prototype.parseからArrayBufferからUint8Arrayでviewを作ってパース。
  parse: function(){
if(this.cache.parse) return this.cache.parse;
return getBox(new Uint8Array(this.data), -8, this.data.byteLength);
}

子ボックスをもつボックスは、基本的に以下のgetBox関数を使えば大丈夫なはず。
function getBox(bytes, offset, size){
var ret = {size: size},
last = offset + size,
boxInfo, box;

offset += 8;
while(offset < last){
boxInfo = getBoxInfo(bytes, offset);
box = boxes[boxInfo.type];
if(box) {
if(ret[boxInfo.type] && !isType(ret[boxInfo.type], Array)){
ret[boxInfo.type] = [ret[boxInfo.type]];
ret[boxInfo.type].push(box(bytes, offset, boxInfo.size));
} else if(isType(ret[boxInfo.type], Array)){
ret[boxInfo.type].push(box(bytes, offset, boxInfo.size));
} else {
ret[boxInfo.type] = box(bytes, offset, boxInfo.size);
}
} else {
break;
}
offset += boxInfo.size;
}

return ret;
}

  boxes = {
ID32: getBox,
albm: function(bytes, offset, size){},
auth: function(bytes, offset, size){},
//中略...
trak: getBox,
tref: getBox,
trex: function(bytes, offset, size){},
trgr: function(bytes, offset, size){},
trun: function(bytes, offset, size){},
tsel: function(bytes, offset, size){},
udta: getBox,
uinf: function(bytes, offset, size){},
UITS: function(bytes, offset, size){},
ulst: function(bytes, offset, size){},
'url ': function(bytes, offset, size){},
vmhd: function(bytes, offset, size){},
vwdi: function(bytes, offset, size){},
'xml ': function(bytes, offset, size){},
yrrc: function(bytes, offset, size){},

//codecs
mp4a: function(bytes, offset, size){
return {
dataReferenceIndex: getIntBE(bytes, offset += 12),
channels: getShortBE(bytes, offset += 12),
bitPerSample: getShortBE(bytes, offset += 2),
sampleRate: getIntBE(bytes, offset += 4),
esds: {
objectTypeIndication: bytes[offset += 25],
bufferSizeDB: getShortBE(bytes, offset += 3),
maxBitrate: getIntBE(bytes, offset += 2),
avgBitrate: getIntBE(bytes, offset += 4)
}
}
}
}


これでMP4ファイルがパース出来ました。次に、パースした情報を元にAACを抽出してみます。

mp4.png

MP4ファイルでは、Chunkという単位でぶつ切りにして動画と音声が格納されています。このようにすることによって、インターネットで途中までしか読み込んでいなくても再生できるわけです。さらに動画、音声はそれぞれSampleというもっと小さい単位で分割されていて、Chunkには複数のSampleが格納されています。

各SampleはSampleToChunkBox、SampleSizeBox、ChunkOffsetBoxというボックスの情報をもとに抽出します。

SampleToChunkBoxにはfirstChunk、samplesPerChunksという要素の配列が格納されています。これは、「n番目のfirstChunkの位置からn+1番目のfirstChunkまではChunkごとにsamplesPerChunks個入っている」ということを表しています。

SampleSizeBoxには単純に各Sampleのサイズが格納されています。

ChunkOffsetBoxにはChunkのオフセット(何バイト目から始まるか)が格納されています。

AACの場合は、抽出した各Sampleごとに7バイトのヘッダーをつけて連結すれば完成です。

Mp4.prototype.extractAAC
  extractAAC: function(){
var tree = this.parse(),
tracks = tree.moov.trak,
audioTrack, mp4a, sampleToChunkEntries, sampleSizeEntries, chunkEntries,
i, j, k, n, m, l, fileSize, idx,
offset = 0,
bb = new BlobBuilder(),
aacHeader = new Uint8Array(new ArrayBuffer(7));

if(isType(tracks, Array)){
tracks.forEach(function(track){
if(track.mdia.hdlr.type === 'soun'){
audioTrack = track;
}
});
} else {
if(tracks.mdia.hdlr.type === 'soun'){
audioTrack = track;
} else {
throw 'This file does not have audio files.';
}
}

mp4a = audioTrack.mdia.minf.stbl.stsd.mp4a;
sampleToChunkEntries = audioTrack.mdia.minf.stbl.stsc.body;
sampleSizeEntries = audioTrack.mdia.minf.stbl.stsz.body;
chunkEntries = audioTrack.mdia.minf.stbl.stco.body;

aacHeader[0] = 0xFF;
aacHeader[1] = 0xF9;
aacHeader[2] = 0x40 | (sampleRateTable[mp4a.sampleRate] << 2) | (mp4a.channels >> 2);
aacHeader[6] = 0xFC;

for(i = 0, idx = 0, n = sampleToChunkEntries.length; i < n; ++i){
j = sampleToChunkEntries[i].firstChunk - 1;
m = i + 1 < n ? sampleToChunkEntries[i + 1].firstChunk - 1 : chunkEntries.length;
for(;j < m; ++j){
offset = chunkEntries[j];
for(k = 0, l = sampleToChunkEntries[i].samplesPerChunk; k < l; ++k, ++idx){
//AAC header.
fileSize = sampleSizeEntries[idx] + 7;
aacHeader[3] = (mp4a.channels << 6) | (fileSize >> 11);
aacHeader[4] = fileSize >> 3;
aacHeader[5] = (fileSize << 5) | (0x7ff >> 6);
bb.append(aacHeader.buffer);

//AAC body.
bb.append(this.blob.webkitSlice(offset, offset += sampleSizeEntries[idx]));
}
}
}

return bb.getBlob();
}

DEMO(MP4ファイルのパース結果を表示して抽出したAACをダウンロードする。ダウンロードしたファイルにhoge.aacとか名前をつけてね。あと、Chromeでしか動かない。):
http://dl.dropbox.com/u/142237/JsBlobBuilderSample/example/parsemp4.html

ソースコード:
ukyo/mp4.js - GitHub

参考:
デジタル映像の「アーカイブ&デリバリー」に関する技術情報サイト|mpeg.co.jp > MPEGラボ > 第26回
なんちゃって記 FAAD2を使ってAAC再生ソフトを作る(その1)
ウノウラボ by Zynga Japan: MP4/3GPP/3GPP2ファイルフォーマットの基礎知識
The 'MP4' Registration Authority



posted by 右京 | javascript
blog comments powered by Disqus
×

この広告は90日以上新しい記事の投稿がないブログに表示されております。