diff --git a/lib/build.gradle b/lib/build.gradle index e0a7449e..ac8777cc 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -20,5 +20,6 @@ android { } dependencies { + compile 'org.jcodec:jcodec:0.1.9' compile fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AVCMP4Mux.java b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AVCMP4Mux.java new file mode 100644 index 00000000..ea17d31f --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AVCMP4Mux.java @@ -0,0 +1,119 @@ +package net.ypresto.androidtranscoder.muxer; + +import org.jcodec.codecs.h264.H264Utils; +import org.jcodec.codecs.h264.MappedH264ES; +import org.jcodec.codecs.h264.io.model.PictureParameterSet; +import org.jcodec.codecs.h264.io.model.SeqParameterSet; +import org.jcodec.codecs.h264.mp4.AvcCBox; +import org.jcodec.common.NIOUtils; +import org.jcodec.common.SeekableByteChannel; +import org.jcodec.common.model.Packet; +import org.jcodec.common.model.Size; +import org.jcodec.common.tools.MainUtils; +import org.jcodec.common.tools.MainUtils.Cmd; +import org.jcodec.containers.mp4.MP4Packet; +import org.jcodec.containers.mp4.TrackType; +import org.jcodec.containers.mp4.boxes.SampleEntry; +import org.jcodec.containers.mp4.muxer.FramesMP4MuxerTrack; +import org.jcodec.containers.mp4.muxer.MP4Muxer; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.jcodec.codecs.h264.H264Utils.getPicHeightInMbs; +import static org.jcodec.common.NIOUtils.writableFileChannel; + +/** + * This class is part of JCodec ( www.jcodec.org ) This software is distributed + * under FreeBSD License + * + * Sample code. Muxes H.264 ( MPEG4 AVC ) elementary stream into MP4 ( ISO + * 14496-1/14496-12/14496-14, Quicktime ) container + * + * @author Jay Codec + * + */ +// blob: 5284521e9747c0fc748b1879d78a680532926e0a +// modified: removed unused main(), change encoderName, package-nize static methods +class AVCMP4Mux { + private static AvcCBox avcC; + + static void main(String[] args) throws Exception { + Cmd cmd = MainUtils.parseArguments(args); + if (cmd.argsLength() < 2) { + MainUtils.printHelp(new HashMap() { + { + put("q", "Look for stream parameters only in the beginning of stream"); + } + }, "in.264", "out.mp4"); + System.exit(-1); + } + + File in = new File(cmd.getArg(0)); + File out = new File(cmd.getArg(1)); + + SeekableByteChannel file = writableFileChannel(out); + MP4Muxer muxer = new MP4Muxer(file); + FramesMP4MuxerTrack track = muxer.addTrack(TrackType.VIDEO, 25); + + mux(track, in); + + muxer.writeHeader(); + + file.close(); + } + + static void mux(FramesMP4MuxerTrack track, File f) throws IOException { + MappedH264ES es = new MappedH264ES(NIOUtils.map(f)); + + ArrayList spsList = new ArrayList(); + ArrayList ppsList = new ArrayList(); + Packet frame = null; + while ((frame = es.nextFrame()) != null) { + ByteBuffer data = NIOUtils.cloneBuffer(frame.getData()); + H264Utils.wipePS(data, spsList, ppsList); + H264Utils.encodeMOVPacket(data); + MP4Packet pkt = new MP4Packet(new Packet(frame, data), frame.getPts(), 0); + System.out.println(pkt.getFrameNo()); + track.addFrame(pkt); + } + addSampleEntry(track, es.getSps(), es.getPps()); + } + + static void addSampleEntry(FramesMP4MuxerTrack track, SeqParameterSet[] spss, PictureParameterSet[] ppss) { + SeqParameterSet sps = spss[0]; + Size size = new Size((sps.pic_width_in_mbs_minus1 + 1) << 4, getPicHeightInMbs(sps) << 4); + + SampleEntry se = MP4Muxer.videoSampleEntry("avc1", size, "AndroidTranscoder"); + + avcC = new AvcCBox(sps.profile_idc, 0, sps.level_idc, 4, write(spss), write(ppss)); + se.add(avcC); + track.addSampleEntry(se); + } + + static List write(PictureParameterSet[] ppss) { + List result = new ArrayList(); + for (PictureParameterSet pps : ppss) { + ByteBuffer buf = ByteBuffer.allocate(1024); + pps.write(buf); + buf.flip(); + result.add(buf); + } + return result; + } + + static List write(SeqParameterSet[] spss) { + List result = new ArrayList(); + for (SeqParameterSet sps : spss) { + ByteBuffer buf = ByteBuffer.allocate(1024); + sps.write(buf); + buf.flip(); + result.add(buf); + } + return result; + } +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AndroidMuxer.java b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AndroidMuxer.java new file mode 100644 index 00000000..03980c37 --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/AndroidMuxer.java @@ -0,0 +1,58 @@ +package net.ypresto.androidtranscoder.muxer; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.media.MediaMuxer; +import android.os.Build; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class AndroidMuxer implements Muxer { + private static final String TAG = "AndroidMuxer"; + private MediaMuxer mMediaMuxer; + + public AndroidMuxer(String path, int format) throws IOException { + mMediaMuxer = new MediaMuxer(path, format); + } + + @Override + public void setOrientationHint(int degrees) { + mMediaMuxer.setOrientationHint(degrees); + } + + @Override + public void setLocation(float latitude, float longitude) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + mMediaMuxer.setLocation(latitude, longitude); + } else { + Log.w(TAG, "setLocation: skipped because it is NOT supported by Android API level of this device."); + } + } + + @Override + public void start() { + mMediaMuxer.start(); + } + + @Override + public void stop() { + mMediaMuxer.stop(); + } + + @Override + public int addTrack(MediaFormat format) { + return mMediaMuxer.addTrack(format); + } + + @Override + public void writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo) { + mMediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo); + } + + @Override + public void release() { + mMediaMuxer.release(); + } +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/muxer/JCodecMuxer.java b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/JCodecMuxer.java new file mode 100644 index 00000000..59d0283a --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/JCodecMuxer.java @@ -0,0 +1,115 @@ +package net.ypresto.androidtranscoder.muxer; + +import android.media.MediaCodec; +import android.media.MediaFormat; + +import net.ypresto.androidtranscoder.utils.MediaFormatUtils; + +import org.jcodec.codecs.h264.io.model.SeqParameterSet; +import org.jcodec.codecs.h264.mp4.AvcCBox; +import org.jcodec.common.FileChannelWrapper; +import org.jcodec.common.NIOUtils; +import org.jcodec.common.model.Size; +import org.jcodec.containers.mp4.MP4Packet; +import org.jcodec.containers.mp4.TrackType; +import org.jcodec.containers.mp4.boxes.SampleEntry; +import org.jcodec.containers.mp4.muxer.FramesMP4MuxerTrack; +import org.jcodec.containers.mp4.muxer.MP4Muxer; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class JCodecMuxer implements Muxer { + private final FileChannelWrapper mFileChannelWrapper; + private final MP4Muxer mMP4Muxer; + private final List mTrackContainers = new ArrayList(); + + public JCodecMuxer(String path, int bufferSize) throws IOException { + File out = new File(path); + mFileChannelWrapper = NIOUtils.writableFileChannel(out); + mMP4Muxer = new MP4Muxer(mFileChannelWrapper); + } + + @Override + public void setOrientationHint(int degrees) { + } + + @Override + public void setLocation(float latitude, float longitude) { + + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public int addTrack(MediaFormat format) { + int index = mTrackContainers.size(); + String mime = MediaFormatUtils.getMime(format); + if (mime.startsWith("video/")) { + mTrackContainers.add(addVideoTrack(format)); + } else if (mime.startsWith("audio/")) { + mTrackContainers.add(addAudioTrack(format)); + } else { + throw new UnsupportedOperationException("Unsupported track mime type: " + mime); + } + return index; + } + + @Override + public void writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo) { + byteBuf.position(bufferInfo.offset); + byteBuf.limit(bufferInfo.offset + bufferInfo.size); + TrackContainer trackContainer = mTrackContainers.get(trackIndex); + trackContainer.mTrack.addFrame(new MP4Packet(byteBuf, bufferInfo.presentationTimeUs, )); + } + + @Override + public void release() { + + } + + private TrackContainer addVideoTrack(MediaFormat format) { + // Refer: AVCMP4Mux.java in jcodec + + SeqParameterSet sps = SeqParameterSet.read(MediaFormatUtils.getSpsBuffer(format)); + int timeScale = sps.vuiParams.time_scale; + + FramesMP4MuxerTrack track = mMP4Muxer.addTrack(TrackType.VIDEO, timeScale); + Size size = new Size(MediaFormatUtils.getWidth(format), MediaFormatUtils.getHeight(format)); + SampleEntry se = MP4Muxer.videoSampleEntry("avc1", size, "AndroidTranscoder"); + AvcCBox avcC = new AvcCBox(sps.profile_idc, 0, sps.level_idc, 4, + Arrays.asList(MediaFormatUtils.getSpsBuffer(format)), + Arrays.asList(MediaFormatUtils.getPpsBuffer(format))); + se.add(avcC); + track.addSampleEntry(se); + + return new TrackContainer(track, timeScale); + } + + private FramesMP4MuxerTrack addAudioTrack(MediaFormat format) { + // TODO + return null; + } + + private static class TrackContainer { + public final FramesMP4MuxerTrack mTrack; + public final int mTimeScale; + + public TrackContainer(FramesMP4MuxerTrack track, int timeScale) { + mTrack = track; + mTimeScale = timeScale; + } + } +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/muxer/Muxer.java b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/Muxer.java new file mode 100644 index 00000000..b2e3251a --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/Muxer.java @@ -0,0 +1,25 @@ +package net.ypresto.androidtranscoder.muxer; + +import android.media.MediaCodec; +import android.media.MediaFormat; + +import java.nio.ByteBuffer; + +/** + * Abstraction interface to use non-Android muxer in same interface as {@link android.media.MediaMuxer}. + */ +public interface Muxer { + void setOrientationHint(int degrees); + + void setLocation(float latitude, float longitude); + + void start(); + + void stop(); + + int addTrack(MediaFormat format); + + void writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo); + + void release(); +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/muxer/MuxerFactory.java b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/MuxerFactory.java new file mode 100644 index 00000000..92cbdb15 --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/muxer/MuxerFactory.java @@ -0,0 +1,7 @@ +package net.ypresto.androidtranscoder.muxer; + +/** + * Created by yuya.tanaka on 2014/11/07. + */ +public class MuxerFactory { +} diff --git a/lib/src/main/java/net/ypresto/androidtranscoder/utils/MediaFormatUtils.java b/lib/src/main/java/net/ypresto/androidtranscoder/utils/MediaFormatUtils.java new file mode 100644 index 00000000..0819c027 --- /dev/null +++ b/lib/src/main/java/net/ypresto/androidtranscoder/utils/MediaFormatUtils.java @@ -0,0 +1,31 @@ +package net.ypresto.androidtranscoder.utils; + +import android.media.MediaFormat; + +import java.nio.ByteBuffer; + +public class MediaFormatUtils { + + private MediaFormatUtils() { + } + + public static String getMime(MediaFormat format) { + return format.getString(MediaFormat.KEY_MIME); + } + + public static int getWidth(MediaFormat format) { + return format.getInteger(MediaFormat.KEY_WIDTH); + } + + public static int getHeight(MediaFormat format) { + return format.getInteger(MediaFormat.KEY_HEIGHT); + } + + public static ByteBuffer getSpsBuffer(MediaFormat format) { + return format.getByteBuffer("csd-0").duplicate(); + } + + public static ByteBuffer getPpsBuffer(MediaFormat format) { + return format.getByteBuffer("csd-1").duplicate(); + } +}