$ 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); } }