$ sudo yum -y install gpac $ sudo yum -y install ffmpeg
MP4box は、GPAC(マルチメディアライブラリ) を構成するツールの一つ。ヘッダ情報(ボックス)を操作する
#!/bin/bash
################################################################
# 3GPP 変換シェル
#
# 使い方: to3gpp.sh [変換元ファイル名]
#
################################################################
SRC_FILE=$1
DEST_FILE=${SRC_FILE%.mp3}
DEST_FILE=${DEST_FILE%.m4a}
DEST_FILE=${DEST_FILE%.avi}
DEST_FILE=$DEST_FILE
/usr/bin/ffmpeg -y -i "$SRC_FILE"\
-vn -acodec aac -profile aac_low -ab 80k -ar 16000 -ac 1\
-f 3gp "$DEST_FILE.aac"
/usr/bin/MP4Box -add "$DEST_FILE.aac" -brand mmp4:1 -new "$DEST_FILE.3gp"
rm "$DEST_FILE.aac"
<3GPPファイル> ::= <ボックス>+ <ボックス> ::= <ボックス容量> <ボックス名> <コンテンツ> <ボックス容量> ::= 4byteの整数 <ボックス名> ::= 4byteのASCII文字列 <コンテンツ> ::= <ボックス>+ | バイナリデータ3GPPファイルは、ボックスから構成されており、ボックスの先頭 4byte はボックス容量、次の 4byte にボックス名、その後にコンテンツが入る。コンテンツがボックス構造をしていてもよい。
+--ftyp +--moov | +--mvhd | +--drm | | +--dcmd | +--trak | | +--tkhd | | +--mdia | | +--(以下省略) | +--trak | | +--tkhd | | +--mdia | | +--(以下省略) | +--udta | +--titl +--mdat(参考文献より引用)
package com.snail.t3gpp;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Hello world!
*
*/
public class App {
private static final int DRM_DCMD_BYTES = 18;
private static final byte[] DRM_BOX_SIZE = new byte[] { 0x00, 0x00, 0x00, 0x12 }; // 18byte
private static final byte[] DRM_BOX_NAME = "drm ".getBytes();
private static final byte[] DCMD_BOX_SIZE = new byte[] { 0x00, 0x00, 0x00, 0x0a }; // 10byte
private static final byte[] DCMD_BOX_NAME = "dcmd".getBytes();
public static void main(String[] args) {
String inFileName = args[0];
String outFileName = args[1];
try {
File inFile = new File(inFileName);
InputStream is = new FileInputStream(inFile);
OutputStream os = new FileOutputStream(new File(outFileName));
// --------------------------------------------------
// moov 以外は単純にコピーするプロセッサ
// --------------------------------------------------
new MP4Processor("moov") {
/**
* moov が見つかったときの処理
*/
@Override
protected void pointCut(int boxSize, String boxName,
InputStream is, OutputStream os, long srcFileSize)
throws IOException {
// moov ボックス内の mvhd ボックスの次に、[drm[dcmd=000?]] ボックスを追加する
writeBoxSize(os, boxSize + DRM_DCMD_BYTES);
writeBoxName(os, boxName);
// moov ボックスのコンテンツを切り出す
ByteArrayOutputStream moovBoxContents = new ByteArrayOutputStream();
copyBoxContents(is, moovBoxContents, boxSize);
// --------------------------------------------------
// mvhd 以外は単純にコピーするプロセッサ
// --------------------------------------------------
new MP4Processor("mvhd") {
/**
* mvhd が見つかったときの処理
*/
@Override
protected void pointCut(int boxSize, String boxName,
InputStream is, OutputStream os,
long srcFileSize) throws IOException {
// mvhd を出力
writeBoxSize(os, boxSize);
writeBoxName(os, boxName);
copyBoxContents(is, os, boxSize);
// 続けて[drm[dcmd=000?]]を出力
os.write(DRM_BOX_SIZE);
os.write(DRM_BOX_NAME);
os.write(DCMD_BOX_SIZE);
os.write(DCMD_BOX_NAME);
// 0 0 0 0 1 0 0 0 (着うたを許可(8))
// 0 0 0 0 0 1 0 0 (ファイル長が奇数(4))
// 0 0 0 0 0 0 1 0 (音声のみ(2))
// +) 0 0 0 0 0 0 0 1 (SDへのコピー禁止(1))
// ----------------------------
// DCMD制御フラグ
boolean permitChaku = true;
boolean isAudilOnly = true;
boolean denySdCopy = false;
byte dcmdFlag = (byte) (
(permitChaku ? 8 : 0)
+ (isOdd(srcFileSize + DRM_DCMD_BYTES) ? 4 : 0)
+ (isAudilOnly ? 2 : 0)
+ (denySdCopy ? 1 : 0));
os.write(new byte[] { 0x00, dcmdFlag });
}
}.process(
new ByteArrayInputStream(moovBoxContents
.toByteArray()), os, srcFileSize);
}
}.process(is, os, inFile.length());
os.close();
is.close();
} catch (Exception ex) {
ex.printStackTrace();
System.exit(-1);
}
}
}
package com.snail.t3gpp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
public abstract class MP4Processor {
private static final int BUF_SIZE = 1024;
private static final int BOX_SIZE_BYTES = 4;
private static final int BOX_NAME_BYTES = 4;
private String[] pPointCutBoxNameArray = new String[0];
/**
* デフォルトコンストラクタを使えないようにする.
*/
private MP4Processor() {
super();
}
/**
* コンストラクタ
*
* @param pointCutBoxNames
* 特殊な処理をするBOX名
*/
public MP4Processor(final String... pointCutBoxNames) {
this();
pPointCutBoxNameArray = pointCutBoxNames;
}
/**
* 特殊処理(BOX名がコンストラクタで指定したpointCutBoxNamesのとき呼ばれる).
*
* @param boxSize
* @param boxName
* @param is
* @param os
* @param srcFileSize
* @throws IOException
*/
protected abstract void pointCut(final int boxSize, final String boxName,
final InputStream is, final OutputStream os, final long srcFileSize)
throws IOException;
/**
* 通常は is から os にコピーする。コンストラクタで指定した pointCutBoxNames を見つけたら pointCut()
* を呼び出す。
*
* @param is
* @param os
* @param srcFileSize
* @throws IOException
*/
public void process(final InputStream is, final OutputStream os,
final long srcFileSize) throws IOException {
int boxSize = 0;
WHILE_LOOP: while ((boxSize = readBoxSize(is)) > 0) {
String boxName = readBoxName(is);
for (String pointCutBoxName : pPointCutBoxNameArray) {
if (pointCutBoxName.equals(boxName)) {
// 特殊処理が必要な BOX
pointCut(boxSize, boxName, is, os, srcFileSize);
continue WHILE_LOOP;
}
}
// 通常のBOXはそのままコピーする
writeBoxSize(os, boxSize);
writeBoxName(os, boxName);
copyBoxContents(is, os, boxSize);
}
}
protected static void writeBoxName(final OutputStream os,
final String boxName) throws UnsupportedEncodingException,
IOException {
os.write(boxName.getBytes("US-ASCII"));
}
protected static void writeBoxSize(final OutputStream os, final int size)
throws IOException {
os.write((byte) ((size >>> 24) & 0xff));
os.write((byte) ((size >>> 16) & 0xff));
os.write((byte) ((size >>> 8) & 0xff));
os.write((byte) ((size) & 0xff));
}
/**
* is から BOX容量を読み取ります.
*
* @param is
* 変換元ファイル
* @return BOX容量 (isから読み取れなかった場合には -1 を返す)
* @throws IOException
* ファイル読み込みエラー
*/
private static int readBoxSize(InputStream is) throws IOException {
byte[] boxSizeArray = new byte[BOX_SIZE_BYTES];
int readSize = is.read(boxSizeArray);
// BOX容量を読み出せなかった
if (readSize < BOX_SIZE_BYTES) {
return -1;
}
return convBoxSize2Int(boxSizeArray);
}
/**
* is から BOX名を読み取り os に書き込みます.BOX名を返します.
*
* @param is
* 変換元ファイル
* @return BOX名
* @throws IOException
* ファイル読み込みエラー
*/
private static String readBoxName(InputStream is) throws IOException {
byte[] boxNameArray = new byte[BOX_NAME_BYTES];
is.read(boxNameArray);
return new String(boxNameArray, "US-ASCII");
}
/**
* is から BOXのコンテンツを読み込んで os に書き出します.
*
* @param is
* 変換元ファイル
* @param os
* 変換先ファイル
* @param size
* BOX容量(コピーするのは、size - BOX_SIZE_BYTES - BOX_NAME_BYTES バイト)
* @throws IOException
* ファイル書き込み・読み込みエラー
*/
protected static void copyBoxContents(final InputStream is,
final OutputStream os, final int size) throws IOException {
int remain = size - BOX_SIZE_BYTES - BOX_NAME_BYTES;
int readSize;
byte[] buf = new byte[BUF_SIZE];
while (remain > 0) {
// 残りが BUF_SIZE 以上なら BUF_SIZE バイト読み込む
readSize = is.read(buf, 0, (remain > BUF_SIZE ? BUF_SIZE : remain));
os.write(buf, 0, readSize);
remain -= readSize;
}
}
/**
* BOX容量を表すバイト列をint型に変換します.
*
* @param data
* BOX容量を表すバイト列
* @return int型のBOX容量
*/
private static int convBoxSize2Int(final byte[] data) {
int ret = 0;
for (byte datum : data) {
ret = ret * 256 + ((int) datum & 0xff);
}
return ret;
}
/**
* 引数が奇数かどうかを調べます.
*
* @param val
* 検証する値
* @return true:奇数<br/>
* false:偶数
*/
protected static boolean isOdd(final long val) {
// 2進数で表したとき、LSBが 1 なら奇数
return ((val & 1L) == 1L);
}
}