aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/pixart/messenger/utils/video
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/pixart/messenger/utils/video')
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/InputSurface.java136
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/MP4Builder.java445
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/MediaController.java633
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/Mp4Movie.java81
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/OutputSurface.java207
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/Sample.java27
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/TextureRenderer.java213
-rw-r--r--src/main/java/de/pixart/messenger/utils/video/Track.java263
8 files changed, 2005 insertions, 0 deletions
diff --git a/src/main/java/de/pixart/messenger/utils/video/InputSurface.java b/src/main/java/de/pixart/messenger/utils/video/InputSurface.java
new file mode 100644
index 000000000..3376b4b87
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/InputSurface.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLExt;
+import android.opengl.EGLSurface;
+import android.os.Build;
+import android.view.Surface;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class InputSurface {
+ private static final boolean VERBOSE = false;
+ private static final int EGL_RECORDABLE_ANDROID = 0x3142;
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+ private EGLDisplay mEGLDisplay;
+ private EGLContext mEGLContext;
+ private EGLSurface mEGLSurface;
+ private Surface mSurface;
+
+ public InputSurface(Surface surface) {
+ if (surface == null) {
+ throw new NullPointerException();
+ }
+ mSurface = surface;
+ eglSetup();
+ }
+
+ private void eglSetup() {
+ mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+ throw new RuntimeException("unable to get EGL14 display");
+ }
+ int[] version = new int[2];
+ if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
+ mEGLDisplay = null;
+ throw new RuntimeException("unable to initialize EGL14");
+ }
+
+ int[] attribList = {
+ EGL14.EGL_RED_SIZE, 8,
+ EGL14.EGL_GREEN_SIZE, 8,
+ EGL14.EGL_BLUE_SIZE, 8,
+ EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_RECORDABLE_ANDROID, 1,
+ EGL14.EGL_NONE
+ };
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
+ numConfigs, 0)) {
+ throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
+ }
+
+ int[] attrib_list = {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL14.EGL_NONE
+ };
+
+ mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0);
+ checkEglError("eglCreateContext");
+ if (mEGLContext == null) {
+ throw new RuntimeException("null context");
+ }
+
+ int[] surfaceAttribs = {
+ EGL14.EGL_NONE
+ };
+ mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface,
+ surfaceAttribs, 0);
+ checkEglError("eglCreateWindowSurface");
+ if (mEGLSurface == null) {
+ throw new RuntimeException("surface was null");
+ }
+ }
+
+ public void release() {
+ if (EGL14.eglGetCurrentContext().equals(mEGLContext)) {
+ EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
+ }
+ EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
+ EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
+ mSurface.release();
+ mEGLDisplay = null;
+ mEGLContext = null;
+ mEGLSurface = null;
+ mSurface = null;
+ }
+
+ public void makeCurrent() {
+ if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+ }
+ }
+
+ public boolean swapBuffers() {
+ return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);
+ }
+
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ public void setPresentationTime(long nsecs) {
+ EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);
+ }
+
+ private void checkEglError(String msg) {
+ boolean failed = false;
+ int error;
+ while ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
+ failed = true;
+ }
+ if (failed) {
+ throw new RuntimeException("EGL error encountered (see log)");
+ }
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/MP4Builder.java b/src/main/java/de/pixart/messenger/utils/video/MP4Builder.java
new file mode 100644
index 000000000..ee8b83e27
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/MP4Builder.java
@@ -0,0 +1,445 @@
+/*
+ * This is the source code of Telegram for Android v. 1.7.x.
+ * It is licensed under GNU GPL v. 2 or later.
+ * You should have received a copy of the license in this archive (see LICENSE).
+ *
+ * Copyright Nikolai Kudashov, 2013-2014.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+
+import com.coremedia.iso.BoxParser;
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.IsoTypeWriter;
+import com.coremedia.iso.boxes.Box;
+import com.coremedia.iso.boxes.Container;
+import com.coremedia.iso.boxes.DataEntryUrlBox;
+import com.coremedia.iso.boxes.DataInformationBox;
+import com.coremedia.iso.boxes.DataReferenceBox;
+import com.coremedia.iso.boxes.FileTypeBox;
+import com.coremedia.iso.boxes.HandlerBox;
+import com.coremedia.iso.boxes.MediaBox;
+import com.coremedia.iso.boxes.MediaHeaderBox;
+import com.coremedia.iso.boxes.MediaInformationBox;
+import com.coremedia.iso.boxes.MovieBox;
+import com.coremedia.iso.boxes.MovieHeaderBox;
+import com.coremedia.iso.boxes.SampleSizeBox;
+import com.coremedia.iso.boxes.SampleTableBox;
+import com.coremedia.iso.boxes.SampleToChunkBox;
+import com.coremedia.iso.boxes.StaticChunkOffsetBox;
+import com.coremedia.iso.boxes.SyncSampleBox;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.coremedia.iso.boxes.TrackBox;
+import com.coremedia.iso.boxes.TrackHeaderBox;
+import com.googlecode.mp4parser.DataSource;
+import com.googlecode.mp4parser.util.Matrix;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+@TargetApi(16)
+public class MP4Builder {
+
+ private InterleaveChunkMdat mdat = null;
+ private Mp4Movie currentMp4Movie = null;
+ private FileOutputStream fos = null;
+ private FileChannel fc = null;
+ private long dataOffset = 0;
+ private long writedSinceLastMdat = 0;
+ private boolean writeNewMdat = true;
+ private HashMap<Track, long[]> track2SampleSizes = new HashMap<>();
+ private ByteBuffer sizeBuffer = null;
+
+ public MP4Builder createMovie(Mp4Movie mp4Movie) throws Exception {
+ currentMp4Movie = mp4Movie;
+
+ fos = new FileOutputStream(mp4Movie.getCacheFile());
+ fc = fos.getChannel();
+
+ FileTypeBox fileTypeBox = createFileTypeBox();
+ fileTypeBox.getBox(fc);
+ dataOffset += fileTypeBox.getSize();
+ writedSinceLastMdat += dataOffset;
+
+ mdat = new InterleaveChunkMdat();
+
+ sizeBuffer = ByteBuffer.allocateDirect(4);
+
+ return this;
+ }
+
+ private void flushCurrentMdat() throws Exception {
+ long oldPosition = fc.position();
+ fc.position(mdat.getOffset());
+ mdat.getBox(fc);
+ fc.position(oldPosition);
+ mdat.setDataOffset(0);
+ mdat.setContentSize(0);
+ fos.flush();
+ }
+
+ public boolean writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo, boolean isAudio) throws Exception {
+ if (writeNewMdat) {
+ mdat.setContentSize(0);
+ mdat.getBox(fc);
+ mdat.setDataOffset(dataOffset);
+ dataOffset += 16;
+ writedSinceLastMdat += 16;
+ writeNewMdat = false;
+ }
+
+ mdat.setContentSize(mdat.getContentSize() + bufferInfo.size);
+ writedSinceLastMdat += bufferInfo.size;
+
+ boolean flush = false;
+ if (writedSinceLastMdat >= 32 * 1024) {
+ flushCurrentMdat();
+ writeNewMdat = true;
+ flush = true;
+ writedSinceLastMdat -= 32 * 1024;
+ }
+
+ currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo);
+ byteBuf.position(bufferInfo.offset + (isAudio ? 0 : 4));
+ byteBuf.limit(bufferInfo.offset + bufferInfo.size);
+
+ if (!isAudio) {
+ sizeBuffer.position(0);
+ sizeBuffer.putInt(bufferInfo.size - 4);
+ sizeBuffer.position(0);
+ fc.write(sizeBuffer);
+ }
+
+ fc.write(byteBuf);
+ dataOffset += bufferInfo.size;
+
+ if (flush) {
+ fos.flush();
+ }
+ return flush;
+ }
+
+ public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception {
+ return currentMp4Movie.addTrack(mediaFormat, isAudio);
+ }
+
+ public void finishMovie(boolean error) throws Exception {
+ if (mdat.getContentSize() != 0) {
+ flushCurrentMdat();
+ }
+
+ for (Track track : currentMp4Movie.getTracks()) {
+ List<Sample> samples = track.getSamples();
+ long[] sizes = new long[samples.size()];
+ for (int i = 0; i < sizes.length; i++) {
+ sizes[i] = samples.get(i).getSize();
+ }
+ track2SampleSizes.put(track, sizes);
+ }
+
+ Box moov = createMovieBox(currentMp4Movie);
+ moov.getBox(fc);
+ fos.flush();
+
+ fc.close();
+ fos.close();
+ }
+
+ protected FileTypeBox createFileTypeBox() {
+ LinkedList<String> minorBrands = new LinkedList<>();
+ minorBrands.add("isom");
+ minorBrands.add("3gp4");
+ return new FileTypeBox("isom", 0, minorBrands);
+ }
+
+ private class InterleaveChunkMdat implements Box {
+ private Container parent;
+ private long contentSize = 1024 * 1024 * 1024;
+ private long dataOffset = 0;
+
+ public Container getParent() {
+ return parent;
+ }
+
+ public long getOffset() {
+ return dataOffset;
+ }
+
+ public void setDataOffset(long offset) {
+ dataOffset = offset;
+ }
+
+ public void setParent(Container parent) {
+ this.parent = parent;
+ }
+
+ public void setContentSize(long contentSize) {
+ this.contentSize = contentSize;
+ }
+
+ public long getContentSize() {
+ return contentSize;
+ }
+
+ public String getType() {
+ return "mdat";
+ }
+
+ public long getSize() {
+ return 16 + contentSize;
+ }
+
+ private boolean isSmallBox(long contentSize) {
+ return (contentSize + 8) < 4294967296L;
+ }
+
+ @Override
+ public void parse(DataSource dataSource, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
+
+ }
+
+ public void getBox(WritableByteChannel writableByteChannel) throws IOException {
+ ByteBuffer bb = ByteBuffer.allocate(16);
+ long size = getSize();
+ if (isSmallBox(size)) {
+ IsoTypeWriter.writeUInt32(bb, size);
+ } else {
+ IsoTypeWriter.writeUInt32(bb, 1);
+ }
+ bb.put(IsoFile.fourCCtoBytes("mdat"));
+ if (isSmallBox(size)) {
+ bb.put(new byte[8]);
+ } else {
+ IsoTypeWriter.writeUInt64(bb, size);
+ }
+ bb.rewind();
+ writableByteChannel.write(bb);
+ }
+ }
+
+ public static long gcd(long a, long b) {
+ if (b == 0) {
+ return a;
+ }
+ return gcd(b, a % b);
+ }
+
+ public long getTimescale(Mp4Movie mp4Movie) {
+ long timescale = 0;
+ if (!mp4Movie.getTracks().isEmpty()) {
+ timescale = mp4Movie.getTracks().iterator().next().getTimeScale();
+ }
+ for (Track track : mp4Movie.getTracks()) {
+ timescale = gcd(track.getTimeScale(), timescale);
+ }
+ return timescale;
+ }
+
+ protected MovieBox createMovieBox(Mp4Movie movie) {
+ MovieBox movieBox = new MovieBox();
+ MovieHeaderBox mvhd = new MovieHeaderBox();
+
+ mvhd.setCreationTime(new Date());
+ mvhd.setModificationTime(new Date());
+ mvhd.setMatrix(Matrix.ROTATE_0);
+ long movieTimeScale = getTimescale(movie);
+ long duration = 0;
+
+ for (Track track : movie.getTracks()) {
+ long tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale();
+ if (tracksDuration > duration) {
+ duration = tracksDuration;
+ }
+ }
+
+ mvhd.setDuration(duration);
+ mvhd.setTimescale(movieTimeScale);
+ mvhd.setNextTrackId(movie.getTracks().size() + 1);
+
+ movieBox.addBox(mvhd);
+ for (Track track : movie.getTracks()) {
+ movieBox.addBox(createTrackBox(track, movie));
+ }
+ return movieBox;
+ }
+
+ protected TrackBox createTrackBox(Track track, Mp4Movie movie) {
+ TrackBox trackBox = new TrackBox();
+ TrackHeaderBox tkhd = new TrackHeaderBox();
+
+ tkhd.setEnabled(true);
+ tkhd.setInMovie(true);
+ tkhd.setInPreview(true);
+ if (track.isAudio()) {
+ tkhd.setMatrix(Matrix.ROTATE_0);
+ } else {
+ tkhd.setMatrix(movie.getMatrix());
+ }
+ tkhd.setAlternateGroup(0);
+ tkhd.setCreationTime(track.getCreationTime());
+ tkhd.setDuration(track.getDuration() * getTimescale(movie) / track.getTimeScale());
+ tkhd.setHeight(track.getHeight());
+ tkhd.setWidth(track.getWidth());
+ tkhd.setLayer(0);
+ tkhd.setModificationTime(new Date());
+ tkhd.setTrackId(track.getTrackId() + 1);
+ tkhd.setVolume(track.getVolume());
+
+ trackBox.addBox(tkhd);
+
+ MediaBox mdia = new MediaBox();
+ trackBox.addBox(mdia);
+ MediaHeaderBox mdhd = new MediaHeaderBox();
+ mdhd.setCreationTime(track.getCreationTime());
+ mdhd.setDuration(track.getDuration());
+ mdhd.setTimescale(track.getTimeScale());
+ mdhd.setLanguage("eng");
+ mdia.addBox(mdhd);
+ HandlerBox hdlr = new HandlerBox();
+ hdlr.setName(track.isAudio() ? "SoundHandle" : "VideoHandle");
+ hdlr.setHandlerType(track.getHandler());
+
+ mdia.addBox(hdlr);
+
+ MediaInformationBox minf = new MediaInformationBox();
+ minf.addBox(track.getMediaHeaderBox());
+
+ DataInformationBox dinf = new DataInformationBox();
+ DataReferenceBox dref = new DataReferenceBox();
+ dinf.addBox(dref);
+ DataEntryUrlBox url = new DataEntryUrlBox();
+ url.setFlags(1);
+ dref.addBox(url);
+ minf.addBox(dinf);
+
+ Box stbl = createStbl(track);
+ minf.addBox(stbl);
+ mdia.addBox(minf);
+
+ return trackBox;
+ }
+
+ protected Box createStbl(Track track) {
+ SampleTableBox stbl = new SampleTableBox();
+
+ createStsd(track, stbl);
+ createStts(track, stbl);
+ createStss(track, stbl);
+ createStsc(track, stbl);
+ createStsz(track, stbl);
+ createStco(track, stbl);
+
+ return stbl;
+ }
+
+ protected void createStsd(Track track, SampleTableBox stbl) {
+ stbl.addBox(track.getSampleDescriptionBox());
+ }
+
+ protected void createStts(Track track, SampleTableBox stbl) {
+ TimeToSampleBox.Entry lastEntry = null;
+ List<TimeToSampleBox.Entry> entries = new ArrayList<>();
+
+ for (long delta : track.getSampleDurations()) {
+ if (lastEntry != null && lastEntry.getDelta() == delta) {
+ lastEntry.setCount(lastEntry.getCount() + 1);
+ } else {
+ lastEntry = new TimeToSampleBox.Entry(1, delta);
+ entries.add(lastEntry);
+ }
+ }
+ TimeToSampleBox stts = new TimeToSampleBox();
+ stts.setEntries(entries);
+ stbl.addBox(stts);
+ }
+
+ protected void createStss(Track track, SampleTableBox stbl) {
+ long[] syncSamples = track.getSyncSamples();
+ if (syncSamples != null && syncSamples.length > 0) {
+ SyncSampleBox stss = new SyncSampleBox();
+ stss.setSampleNumber(syncSamples);
+ stbl.addBox(stss);
+ }
+ }
+
+ protected void createStsc(Track track, SampleTableBox stbl) {
+ SampleToChunkBox stsc = new SampleToChunkBox();
+ stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
+
+ long lastOffset = -1;
+ int lastChunkNumber = 1;
+ int lastSampleCount = 0;
+
+ int previousWritedChunkCount = -1;
+
+ int samplesCount = track.getSamples().size();
+ for (int a = 0; a < samplesCount; a++) {
+ Sample sample = track.getSamples().get(a);
+ long offset = sample.getOffset();
+ long size = sample.getSize();
+
+ lastOffset = offset + size;
+ lastSampleCount++;
+
+ boolean write = false;
+ if (a != samplesCount - 1) {
+ Sample nextSample = track.getSamples().get(a + 1);
+ if (lastOffset != nextSample.getOffset()) {
+ write = true;
+ }
+ } else {
+ write = true;
+ }
+ if (write) {
+ if (previousWritedChunkCount != lastSampleCount) {
+ stsc.getEntries().add(new SampleToChunkBox.Entry(lastChunkNumber, lastSampleCount, 1));
+ previousWritedChunkCount = lastSampleCount;
+ }
+ lastSampleCount = 0;
+ lastChunkNumber++;
+ }
+ }
+ stbl.addBox(stsc);
+ }
+
+ protected void createStsz(Track track, SampleTableBox stbl) {
+ SampleSizeBox stsz = new SampleSizeBox();
+ stsz.setSampleSizes(track2SampleSizes.get(track));
+ stbl.addBox(stsz);
+ }
+
+ protected void createStco(Track track, SampleTableBox stbl) {
+ ArrayList<Long> chunksOffsets = new ArrayList<>();
+ long lastOffset = -1;
+ for (Sample sample : track.getSamples()) {
+ long offset = sample.getOffset();
+ if (lastOffset != -1 && lastOffset != offset) {
+ lastOffset = -1;
+ }
+ if (lastOffset == -1) {
+ chunksOffsets.add(offset);
+ }
+ lastOffset = offset + sample.getSize();
+ }
+ long[] chunkOffsetsLong = new long[chunksOffsets.size()];
+ for (int a = 0; a < chunksOffsets.size(); a++) {
+ chunkOffsetsLong[a] = chunksOffsets.get(a);
+ }
+
+ StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
+ stco.setChunkOffsets(chunkOffsetsLong);
+ stbl.addBox(stco);
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/MediaController.java b/src/main/java/de/pixart/messenger/utils/video/MediaController.java
new file mode 100644
index 000000000..2f60310a8
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/MediaController.java
@@ -0,0 +1,633 @@
+package de.pixart.messenger.utils.video;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.os.Build;
+import android.util.Log;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+
+import de.pixart.messenger.Config;
+
+@SuppressLint("NewApi")
+public class MediaController {
+
+ public final static String MIME_TYPE = "video/avc";
+ private final static int PROCESSOR_TYPE_OTHER = 0;
+ private final static int PROCESSOR_TYPE_QCOM = 1;
+ private final static int PROCESSOR_TYPE_INTEL = 2;
+ private final static int PROCESSOR_TYPE_MTK = 3;
+ private final static int PROCESSOR_TYPE_SEC = 4;
+ private final static int PROCESSOR_TYPE_TI = 5;
+ private static volatile MediaController Instance = null;
+ private boolean videoConvertFirstWrite = true;
+
+ public static MediaController getInstance() {
+ MediaController localInstance = Instance;
+ if (localInstance == null) {
+ synchronized (MediaController.class) {
+ localInstance = Instance;
+ if (localInstance == null) {
+ Instance = localInstance = new MediaController();
+ }
+ }
+ }
+ return localInstance;
+ }
+
+ @SuppressLint("NewApi")
+ public static int selectColorFormat(MediaCodecInfo codecInfo, String mimeType) {
+ MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
+ int lastColorFormat = 0;
+ for (int i = 0; i < capabilities.colorFormats.length; i++) {
+ int colorFormat = capabilities.colorFormats[i];
+ if (isRecognizedFormat(colorFormat)) {
+ lastColorFormat = colorFormat;
+ if (!(codecInfo.getName().equals("OMX.SEC.AVC.Encoder") && colorFormat == 19)) {
+ return colorFormat;
+ }
+ }
+ }
+ return lastColorFormat;
+ }
+
+ private static boolean isRecognizedFormat(int colorFormat) {
+ switch (colorFormat) {
+ case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
+ case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
+ case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
+ case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
+ case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public native static int convertVideoFrame(ByteBuffer src, ByteBuffer dest, int destFormat, int width, int height, int padding, int swap);
+
+ private void didWriteData(final boolean last, final boolean error) {
+ final boolean firstWrite = videoConvertFirstWrite;
+ if (firstWrite) {
+ videoConvertFirstWrite = false;
+ }
+ }
+
+ public static MediaCodecInfo selectCodec(String mimeType) {
+ int numCodecs = MediaCodecList.getCodecCount();
+ MediaCodecInfo lastCodecInfo = null;
+ for (int i = 0; i < numCodecs; i++) {
+ MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
+ if (!codecInfo.isEncoder()) {
+ continue;
+ }
+ String[] types = codecInfo.getSupportedTypes();
+ for (String type : types) {
+ if (type.equalsIgnoreCase(mimeType)) {
+ lastCodecInfo = codecInfo;
+ if (!lastCodecInfo.getName().equals("OMX.SEC.avc.enc")) {
+ return lastCodecInfo;
+ } else if (lastCodecInfo.getName().equals("OMX.SEC.AVC.Encoder")) {
+ return lastCodecInfo;
+ }
+ }
+ }
+ }
+ return lastCodecInfo;
+ }
+
+ @TargetApi(16)
+ private long readAndWriteTrack(MediaExtractor extractor, MP4Builder mediaMuxer, MediaCodec.BufferInfo info, long start, long end, File file, boolean isAudio) throws Exception {
+ int trackIndex = selectTrack(extractor, isAudio);
+ if (trackIndex >= 0) {
+ extractor.selectTrack(trackIndex);
+ MediaFormat trackFormat = extractor.getTrackFormat(trackIndex);
+ int muxerTrackIndex = mediaMuxer.addTrack(trackFormat, isAudio);
+ int maxBufferSize = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
+ boolean inputDone = false;
+ if (start > 0) {
+ extractor.seekTo(start, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+ } else {
+ extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+ }
+ ByteBuffer buffer = ByteBuffer.allocateDirect(maxBufferSize);
+ long startTime = -1;
+
+ while (!inputDone) {
+
+ boolean eof = false;
+ int index = extractor.getSampleTrackIndex();
+ if (index == trackIndex) {
+ info.size = extractor.readSampleData(buffer, 0);
+
+ if (info.size < 0) {
+ info.size = 0;
+ eof = true;
+ } else {
+ info.presentationTimeUs = extractor.getSampleTime();
+ if (start > 0 && startTime == -1) {
+ startTime = info.presentationTimeUs;
+ }
+ if (end < 0 || info.presentationTimeUs < end) {
+ info.offset = 0;
+ info.flags = extractor.getSampleFlags();
+ if (mediaMuxer.writeSampleData(muxerTrackIndex, buffer, info, isAudio)) {
+ // didWriteData(messageObject, file, false, false);
+ }
+ extractor.advance();
+ } else {
+ eof = true;
+ }
+ }
+ } else if (index == -1) {
+ eof = true;
+ }
+ if (eof) {
+ inputDone = true;
+ }
+ }
+
+ extractor.unselectTrack(trackIndex);
+ return startTime;
+ }
+ return -1;
+ }
+
+ @TargetApi(16)
+ private int selectTrack(MediaExtractor extractor, boolean audio) {
+ int numTracks = extractor.getTrackCount();
+ for (int i = 0; i < numTracks; i++) {
+ MediaFormat format = extractor.getTrackFormat(i);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ if (audio) {
+ if (mime.startsWith("audio/")) {
+ return i;
+ }
+ } else {
+ if (mime.startsWith("video/")) {
+ return i;
+ }
+ }
+ }
+ return -5;
+ }
+
+ @TargetApi(16)
+ public boolean convertVideo(final String path, final String compressed_path) {
+
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(path);
+ String height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+ String width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+ String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+
+ int video_height = Integer.parseInt(height);
+ int video_width = Integer.parseInt(width);
+
+ Log.d(Config.LOGTAG, "Video dimensions: height: " + height + " width: " + width + "rotation: " + rotation);
+
+ long startTime = -1;
+ long endTime = -1;
+
+ int resultWidth = 640;
+ int resultHeight = 360;
+
+ int rotationValue = Integer.valueOf(rotation);
+ int originalWidth = Integer.valueOf(width);
+ int originalHeight = Integer.valueOf(height);
+ double ratio = video_width/video_height;
+
+ int bitrate = 50000; //450000
+ int rotateRender = 0;
+
+ File cacheFile = new File(compressed_path);
+
+ if (Build.VERSION.SDK_INT < 18 && resultHeight > resultWidth && resultWidth != originalWidth && resultHeight != originalHeight) {
+ int temp = resultHeight;
+ resultHeight = resultWidth;
+ resultWidth = temp;
+ rotationValue = 90;
+ rotateRender = 270;
+ } else if (Build.VERSION.SDK_INT > 20) {
+ if (rotationValue == 90) {
+ int temp = resultHeight;
+ resultHeight = resultWidth;
+ resultWidth = temp;
+ rotationValue = 0;
+ rotateRender = 270;
+ } else if (rotationValue == 180) {
+ rotateRender = 180;
+ rotationValue = 0;
+ } else if (rotationValue == 270) {
+ int temp = resultHeight;
+ resultHeight = resultWidth;
+ resultWidth = temp;
+ rotationValue = 0;
+ rotateRender = 90;
+ }
+ }
+
+
+ File inputFile = new File(path);
+ Log.d(Config.LOGTAG, "Input file is: " + inputFile.toString());
+ if (!inputFile.canRead()) {
+ didWriteData(true, true);
+ Log.d(Config.LOGTAG, "Compression failed. Input file could not be read.");
+ return false;
+ }
+
+ videoConvertFirstWrite = true;
+ boolean error = false;
+ long videoStartTime = startTime;
+
+ long time = System.currentTimeMillis();
+
+ if (resultWidth != 0 && resultHeight != 0) {
+ MP4Builder mediaMuxer = null;
+ MediaExtractor extractor = null;
+
+ try {
+ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ Mp4Movie movie = new Mp4Movie();
+ movie.setCacheFile(cacheFile);
+ movie.setRotation(rotationValue);
+ movie.setSize(resultWidth, resultHeight);
+ mediaMuxer = new MP4Builder().createMovie(movie);
+ extractor = new MediaExtractor();
+ extractor.setDataSource(inputFile.toString());
+
+
+ if (resultWidth != originalWidth || resultHeight != originalHeight) {
+ int videoIndex;
+ videoIndex = selectTrack(extractor, false);
+
+ if (videoIndex >= 0) {
+ MediaCodec decoder = null;
+ MediaCodec encoder = null;
+ InputSurface inputSurface = null;
+ OutputSurface outputSurface = null;
+
+ try {
+ long videoTime = -1;
+ boolean outputDone = false;
+ boolean inputDone = false;
+ boolean decoderDone = false;
+ int swapUV = 0;
+ int videoTrackIndex = -5;
+
+ int colorFormat;
+ int processorType = PROCESSOR_TYPE_OTHER;
+ String manufacturer = Build.MANUFACTURER.toLowerCase();
+ if (Build.VERSION.SDK_INT < 18) {
+ MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
+ colorFormat = selectColorFormat(codecInfo, MIME_TYPE);
+ if (colorFormat == 0) {
+ throw new RuntimeException("no supported color format");
+ }
+ String codecName = codecInfo.getName();
+ if (codecName.contains("OMX.qcom.")) {
+ processorType = PROCESSOR_TYPE_QCOM;
+ if (Build.VERSION.SDK_INT == 16) {
+ if (manufacturer.equals("lge") || manufacturer.equals("nokia")) {
+ swapUV = 1;
+ }
+ }
+ } else if (codecName.contains("OMX.Intel.")) {
+ processorType = PROCESSOR_TYPE_INTEL;
+ } else if (codecName.equals("OMX.MTK.VIDEO.ENCODER.AVC")) {
+ processorType = PROCESSOR_TYPE_MTK;
+ } else if (codecName.equals("OMX.SEC.AVC.Encoder")) {
+ processorType = PROCESSOR_TYPE_SEC;
+ swapUV = 1;
+ } else if (codecName.equals("OMX.TI.DUCATI1.VIDEO.H264E")) {
+ processorType = PROCESSOR_TYPE_TI;
+ }
+ Log.d(Config.LOGTAG, "codec = " + codecInfo.getName() + " manufacturer = " + manufacturer + "device = " + Build.MODEL);
+ } else {
+ colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
+ }
+ Log.d(Config.LOGTAG, "colorFormat = " + colorFormat);
+
+ int resultHeightAligned = resultHeight;
+ int padding = 0;
+ int bufferSize = resultWidth * resultHeight * 3 / 2;
+ if (processorType == PROCESSOR_TYPE_OTHER) {
+ if (resultHeight % 16 != 0) {
+ resultHeightAligned += (16 - (resultHeight % 16));
+ padding = resultWidth * (resultHeightAligned - resultHeight);
+ bufferSize += padding * 5 / 4;
+ }
+ } else if (processorType == PROCESSOR_TYPE_QCOM) {
+ if (!manufacturer.toLowerCase().equals("lge")) {
+ int uvoffset = (resultWidth * resultHeight + 2047) & ~2047;
+ padding = uvoffset - (resultWidth * resultHeight);
+ bufferSize += padding;
+ }
+ } else if (processorType == PROCESSOR_TYPE_TI) {
+ resultHeightAligned = 368;
+ bufferSize = resultWidth * resultHeightAligned * 3 / 2;
+ resultHeightAligned += (16 - (resultHeight % 16));
+ padding = resultWidth * (resultHeightAligned - resultHeight);
+ bufferSize += padding * 5 / 4;
+ } else if (processorType == PROCESSOR_TYPE_MTK) {
+ if (manufacturer.equals("baidu")) {
+ resultHeightAligned += (16 - (resultHeight % 16));
+ padding = resultWidth * (resultHeightAligned - resultHeight);
+ bufferSize += padding * 5 / 4;
+ }
+ }
+
+ extractor.selectTrack(videoIndex);
+ if (startTime > 0) {
+ extractor.seekTo(startTime, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+ } else {
+ extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+ }
+ MediaFormat inputFormat = extractor.getTrackFormat(videoIndex);
+
+ MediaFormat outputFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight);
+ outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+ outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate != 0 ? bitrate : 921600);
+ outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
+ outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
+ if (Build.VERSION.SDK_INT < 18) {
+ outputFormat.setInteger("stride", resultWidth + 32);
+ outputFormat.setInteger("slice-height", resultHeight);
+ }
+
+ encoder = MediaCodec.createEncoderByType(MIME_TYPE);
+ encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ if (Build.VERSION.SDK_INT >= 18) {
+ inputSurface = new InputSurface(encoder.createInputSurface());
+ inputSurface.makeCurrent();
+ }
+ encoder.start();
+
+ decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME));
+ if (Build.VERSION.SDK_INT >= 18) {
+ outputSurface = new OutputSurface();
+ } else {
+ outputSurface = new OutputSurface(resultWidth, resultHeight, rotateRender);
+ }
+ decoder.configure(inputFormat, outputSurface.getSurface(), null, 0);
+ decoder.start();
+
+ final int TIMEOUT_USEC = 2500;
+ ByteBuffer[] decoderInputBuffers = null;
+ ByteBuffer[] encoderOutputBuffers = null;
+ ByteBuffer[] encoderInputBuffers = null;
+ if (Build.VERSION.SDK_INT < 21) {
+ decoderInputBuffers = decoder.getInputBuffers();
+ encoderOutputBuffers = encoder.getOutputBuffers();
+ if (Build.VERSION.SDK_INT < 18) {
+ encoderInputBuffers = encoder.getInputBuffers();
+ }
+ }
+
+ while (!outputDone) {
+ if (!inputDone) {
+ boolean eof = false;
+ int index = extractor.getSampleTrackIndex();
+ if (index == videoIndex) {
+ int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
+ if (inputBufIndex >= 0) {
+ ByteBuffer inputBuf;
+ if (Build.VERSION.SDK_INT < 21) {
+ inputBuf = decoderInputBuffers[inputBufIndex];
+ } else {
+ inputBuf = decoder.getInputBuffer(inputBufIndex);
+ }
+ int chunkSize = extractor.readSampleData(inputBuf, 0);
+ if (chunkSize < 0) {
+ decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ inputDone = true;
+ } else {
+ decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, extractor.getSampleTime(), 0);
+ extractor.advance();
+ }
+ }
+ } else if (index == -1) {
+ eof = true;
+ }
+ if (eof) {
+ int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
+ if (inputBufIndex >= 0) {
+ decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ inputDone = true;
+ }
+ }
+ }
+
+ boolean decoderOutputAvailable = !decoderDone;
+ boolean encoderOutputAvailable = true;
+ while (decoderOutputAvailable || encoderOutputAvailable) {
+ int encoderStatus = encoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
+ if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ encoderOutputAvailable = false;
+ } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ if (Build.VERSION.SDK_INT < 21) {
+ encoderOutputBuffers = encoder.getOutputBuffers();
+ }
+ } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ MediaFormat newFormat = encoder.getOutputFormat();
+ if (videoTrackIndex == -5) {
+ videoTrackIndex = mediaMuxer.addTrack(newFormat, false);
+ }
+ } else if (encoderStatus < 0) {
+ throw new RuntimeException("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
+ } else {
+ ByteBuffer encodedData;
+ if (Build.VERSION.SDK_INT < 21) {
+ encodedData = encoderOutputBuffers[encoderStatus];
+ } else {
+ encodedData = encoder.getOutputBuffer(encoderStatus);
+ }
+ if (encodedData == null) {
+ throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
+ }
+ if (info.size > 1) {
+ if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
+ if (mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info, false)) {
+ didWriteData(false, false);
+ }
+ } else if (videoTrackIndex == -5) {
+ byte[] csd = new byte[info.size];
+ encodedData.limit(info.offset + info.size);
+ encodedData.position(info.offset);
+ encodedData.get(csd);
+ ByteBuffer sps = null;
+ ByteBuffer pps = null;
+ for (int a = info.size - 1; a >= 0; a--) {
+ if (a > 3) {
+ if (csd[a] == 1 && csd[a - 1] == 0 && csd[a - 2] == 0 && csd[a - 3] == 0) {
+ sps = ByteBuffer.allocate(a - 3);
+ pps = ByteBuffer.allocate(info.size - (a - 3));
+ sps.put(csd, 0, a - 3).position(0);
+ pps.put(csd, a - 3, info.size - (a - 3)).position(0);
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ MediaFormat newFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight);
+ if (sps != null && pps != null) {
+ newFormat.setByteBuffer("csd-0", sps);
+ newFormat.setByteBuffer("csd-1", pps);
+ }
+ videoTrackIndex = mediaMuxer.addTrack(newFormat, false);
+ }
+ }
+ outputDone = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ encoder.releaseOutputBuffer(encoderStatus, false);
+ }
+ if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
+ continue;
+ }
+
+ if (!decoderDone) {
+ int decoderStatus = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
+ if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ decoderOutputAvailable = false;
+ } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+
+ } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ MediaFormat newFormat = decoder.getOutputFormat();
+ Log.d(Config.LOGTAG, "newFormat = " + newFormat);
+ } else if (decoderStatus < 0) {
+ throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
+ } else {
+ boolean doRender;
+ if (Build.VERSION.SDK_INT >= 18) {
+ doRender = info.size != 0;
+ } else {
+ doRender = info.size != 0 || info.presentationTimeUs != 0;
+ }
+ if (endTime > 0 && info.presentationTimeUs >= endTime) {
+ inputDone = true;
+ decoderDone = true;
+ doRender = false;
+ info.flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ }
+ if (startTime > 0 && videoTime == -1) {
+ if (info.presentationTimeUs < startTime) {
+ doRender = false;
+ Log.d(Config.LOGTAG, "drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs);
+ } else {
+ videoTime = info.presentationTimeUs;
+ }
+ }
+ decoder.releaseOutputBuffer(decoderStatus, doRender);
+ if (doRender) {
+ boolean errorWait = false;
+ try {
+ outputSurface.awaitNewImage();
+ } catch (Exception e) {
+ errorWait = true;
+ Log.d(Config.LOGTAG, e.getMessage());
+ }
+ if (!errorWait) {
+ if (Build.VERSION.SDK_INT >= 18) {
+ outputSurface.drawImage(false);
+ inputSurface.setPresentationTime(info.presentationTimeUs * 1000);
+ inputSurface.swapBuffers();
+ } else {
+ int inputBufIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC);
+ if (inputBufIndex >= 0) {
+ outputSurface.drawImage(true);
+ ByteBuffer rgbBuf = outputSurface.getFrame();
+ ByteBuffer yuvBuf = encoderInputBuffers[inputBufIndex];
+ yuvBuf.clear();
+ convertVideoFrame(rgbBuf, yuvBuf, colorFormat, resultWidth, resultHeight, padding, swapUV);
+ encoder.queueInputBuffer(inputBufIndex, 0, bufferSize, info.presentationTimeUs, 0);
+ } else {
+ Log.d(Config.LOGTAG, "input buffer not available");
+ }
+ }
+ }
+ }
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ decoderOutputAvailable = false;
+ Log.d(Config.LOGTAG, "decoder stream end");
+ if (Build.VERSION.SDK_INT >= 18) {
+ encoder.signalEndOfInputStream();
+ } else {
+ int inputBufIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC);
+ if (inputBufIndex >= 0) {
+ encoder.queueInputBuffer(inputBufIndex, 0, 1, info.presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (videoTime != -1) {
+ videoStartTime = videoTime;
+ }
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, e.getMessage());
+ error = true;
+ }
+
+ extractor.unselectTrack(videoIndex);
+
+ if (outputSurface != null) {
+ outputSurface.release();
+ }
+ if (inputSurface != null) {
+ inputSurface.release();
+ }
+ if (decoder != null) {
+ decoder.stop();
+ decoder.release();
+ }
+ if (encoder != null) {
+ encoder.stop();
+ encoder.release();
+ }
+ }
+ } else {
+ long videoTime = readAndWriteTrack(extractor, mediaMuxer, info, startTime, endTime, cacheFile, false);
+ if (videoTime != -1) {
+ videoStartTime = videoTime;
+ }
+ }
+ if (!error) {
+ readAndWriteTrack(extractor, mediaMuxer, info, videoStartTime, endTime, cacheFile, true);
+ }
+ } catch (Exception e) {
+ error = true;
+ Log.d(Config.LOGTAG, e.getMessage());
+ } finally {
+ if (extractor != null) {
+ extractor.release();
+ }
+ if (mediaMuxer != null) {
+ try {
+ mediaMuxer.finishMovie(false);
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, e.getMessage());
+ }
+ }
+ Log.d(Config.LOGTAG, "time = " + (System.currentTimeMillis() - time));
+ }
+ } else {
+ didWriteData(true, true);
+ Log.d(Config.LOGTAG, "Compression failed.");
+ return false;
+ }
+ didWriteData(true, error);
+ Log.d(Config.LOGTAG, "Compression succeed. Save compressed video to " + cacheFile.toString());
+ //inputFile.delete();
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/utils/video/Mp4Movie.java b/src/main/java/de/pixart/messenger/utils/video/Mp4Movie.java
new file mode 100644
index 000000000..4cf206d26
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/Mp4Movie.java
@@ -0,0 +1,81 @@
+/*
+ * This is the source code of Telegram for Android v. 1.7.x.
+ * It is licensed under GNU GPL v. 2 or later.
+ * You should have received a copy of the license in this archive (see LICENSE).
+ *
+ * Copyright Nikolai Kudashov, 2013-2014.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+
+import com.googlecode.mp4parser.util.Matrix;
+
+import java.io.File;
+import java.util.ArrayList;
+
+@TargetApi(16)
+public class Mp4Movie {
+ private Matrix matrix = Matrix.ROTATE_0;
+ private ArrayList<Track> tracks = new ArrayList<Track>();
+ private File cacheFile;
+ private int width;
+ private int height;
+
+ public Matrix getMatrix() {
+ return matrix;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setCacheFile(File file) {
+ cacheFile = file;
+ }
+
+ public void setRotation(int angle) {
+ if (angle == 0) {
+ matrix = Matrix.ROTATE_0;
+ } else if (angle == 90) {
+ matrix = Matrix.ROTATE_90;
+ } else if (angle == 180) {
+ matrix = Matrix.ROTATE_180;
+ } else if (angle == 270) {
+ matrix = Matrix.ROTATE_270;
+ }
+ }
+
+ public void setSize(int w, int h) {
+ width = w;
+ height = h;
+ }
+
+ public ArrayList<Track> getTracks() {
+ return tracks;
+ }
+
+ public File getCacheFile() {
+ return cacheFile;
+ }
+
+ public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) throws Exception {
+ if (trackIndex < 0 || trackIndex >= tracks.size()) {
+ return;
+ }
+ Track track = tracks.get(trackIndex);
+ track.addSample(offset, bufferInfo);
+ }
+
+ public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception {
+ tracks.add(new Track(tracks.size(), mediaFormat, isAudio));
+ return tracks.size() - 1;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/OutputSurface.java b/src/main/java/de/pixart/messenger/utils/video/OutputSurface.java
new file mode 100644
index 000000000..75ceb46ac
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/OutputSurface.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES20;
+import android.view.Surface;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+
+@TargetApi(16)
+public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener {
+
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private EGL10 mEGL;
+ private EGLDisplay mEGLDisplay = null;
+ private EGLContext mEGLContext = null;
+ private EGLSurface mEGLSurface = null;
+ private SurfaceTexture mSurfaceTexture;
+ private Surface mSurface;
+ private final Object mFrameSyncObject = new Object();
+ private boolean mFrameAvailable;
+ private TextureRenderer mTextureRender;
+ private int mWidth;
+ private int mHeight;
+ private int rotateRender = 0;
+ private ByteBuffer mPixelBuf;
+
+ public OutputSurface(int width, int height, int rotate) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException();
+ }
+ mWidth = width;
+ mHeight = height;
+ rotateRender = rotate;
+ mPixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4);
+ mPixelBuf.order(ByteOrder.LITTLE_ENDIAN);
+ eglSetup(width, height);
+ makeCurrent();
+ setup();
+ }
+
+ public OutputSurface() {
+ setup();
+ }
+
+ private void setup() {
+ mTextureRender = new TextureRenderer(rotateRender);
+ mTextureRender.surfaceCreated();
+ mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId());
+ mSurfaceTexture.setOnFrameAvailableListener(this);
+ mSurface = new Surface(mSurfaceTexture);
+ }
+
+ private void eglSetup(int width, int height) {
+ mEGL = (EGL10) EGLContext.getEGL();
+ mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new RuntimeException("unable to get EGL10 display");
+ }
+
+ if (!mEGL.eglInitialize(mEGLDisplay, null)) {
+ mEGLDisplay = null;
+ throw new RuntimeException("unable to initialize EGL10");
+ }
+
+ int[] attribList = {
+ EGL10.EGL_RED_SIZE, 8,
+ EGL10.EGL_GREEN_SIZE, 8,
+ EGL10.EGL_BLUE_SIZE, 8,
+ EGL10.EGL_ALPHA_SIZE, 8,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_NONE
+ };
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ if (!mEGL.eglChooseConfig(mEGLDisplay, attribList, configs, configs.length, numConfigs)) {
+ throw new RuntimeException("unable to find RGB888+pbuffer EGL config");
+ }
+ int[] attrib_list = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL10.EGL_NONE
+ };
+ mEGLContext = mEGL.eglCreateContext(mEGLDisplay, configs[0], EGL10.EGL_NO_CONTEXT, attrib_list);
+ checkEglError("eglCreateContext");
+ if (mEGLContext == null) {
+ throw new RuntimeException("null context");
+ }
+ int[] surfaceAttribs = {
+ EGL10.EGL_WIDTH, width,
+ EGL10.EGL_HEIGHT, height,
+ EGL10.EGL_NONE
+ };
+ mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs);
+ checkEglError("eglCreatePbufferSurface");
+ if (mEGLSurface == null) {
+ throw new RuntimeException("surface was null");
+ }
+ }
+
+ public void release() {
+ if (mEGL != null) {
+ if (mEGL.eglGetCurrentContext().equals(mEGLContext)) {
+ mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ }
+ mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface);
+ mEGL.eglDestroyContext(mEGLDisplay, mEGLContext);
+ }
+ mSurface.release();
+ mEGLDisplay = null;
+ mEGLContext = null;
+ mEGLSurface = null;
+ mEGL = null;
+ mTextureRender = null;
+ mSurface = null;
+ mSurfaceTexture = null;
+ }
+
+ public void makeCurrent() {
+ if (mEGL == null) {
+ throw new RuntimeException("not configured for makeCurrent");
+ }
+ checkEglError("before makeCurrent");
+ if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+ }
+ }
+
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ public void changeFragmentShader(String fragmentShader) {
+ mTextureRender.changeFragmentShader(fragmentShader);
+ }
+
+ public void awaitNewImage() {
+ final int TIMEOUT_MS = 5000;
+ synchronized (mFrameSyncObject) {
+ while (!mFrameAvailable) {
+ try {
+ mFrameSyncObject.wait(TIMEOUT_MS);
+ if (!mFrameAvailable) {
+ throw new RuntimeException("Surface frame wait timed out");
+ }
+ } catch (InterruptedException ie) {
+ throw new RuntimeException(ie);
+ }
+ }
+ mFrameAvailable = false;
+ }
+ mTextureRender.checkGlError("before updateTexImage");
+ mSurfaceTexture.updateTexImage();
+ }
+
+ public void drawImage(boolean invert) {
+ mTextureRender.drawFrame(mSurfaceTexture, invert);
+ }
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture st) {
+ synchronized (mFrameSyncObject) {
+ if (mFrameAvailable) {
+ throw new RuntimeException("mFrameAvailable already set, frame could be dropped");
+ }
+ mFrameAvailable = true;
+ mFrameSyncObject.notifyAll();
+ }
+ }
+
+ public ByteBuffer getFrame() {
+ mPixelBuf.rewind();
+ GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf);
+ return mPixelBuf;
+ }
+
+ private void checkEglError(String msg) {
+ if (mEGL.eglGetError() != EGL10.EGL_SUCCESS) {
+ throw new RuntimeException("EGL error encountered (see log)");
+ }
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/Sample.java b/src/main/java/de/pixart/messenger/utils/video/Sample.java
new file mode 100644
index 000000000..f00ee5311
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/Sample.java
@@ -0,0 +1,27 @@
+/*
+ * This is the source code of Telegram for Android v. 1.7.x.
+ * It is licensed under GNU GPL v. 2 or later.
+ * You should have received a copy of the license in this archive (see LICENSE).
+ *
+ * Copyright Nikolai Kudashov, 2013-2014.
+ */
+
+package de.pixart.messenger.utils.video;
+
+public class Sample {
+ private long offset = 0;
+ private long size = 0;
+
+ public Sample(long offset, long size) {
+ this.offset = offset;
+ this.size = size;
+ }
+
+ public long getOffset() {
+ return offset;
+ }
+
+ public long getSize() {
+ return size;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/TextureRenderer.java b/src/main/java/de/pixart/messenger/utils/video/TextureRenderer.java
new file mode 100644
index 000000000..aa4542fa3
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/TextureRenderer.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+@TargetApi(16)
+public class TextureRenderer {
+
+ private static final int FLOAT_SIZE_BYTES = 4;
+ private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
+ private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
+ private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
+ private static final float[] mTriangleVerticesData = {
+ -1.0f, -1.0f, 0, 0.f, 0.f,
+ 1.0f, -1.0f, 0, 1.f, 0.f,
+ -1.0f, 1.0f, 0, 0.f, 1.f,
+ 1.0f, 1.0f, 0, 1.f, 1.f,
+ };
+ private FloatBuffer mTriangleVertices;
+
+ private static final String VERTEX_SHADER =
+ "uniform mat4 uMVPMatrix;\n" +
+ "uniform mat4 uSTMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "attribute vec4 aTextureCoord;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uMVPMatrix * aPosition;\n" +
+ " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" +
+ "}\n";
+
+ private static final String FRAGMENT_SHADER =
+ "#extension GL_OES_EGL_image_external : require\n" +
+ "precision mediump float;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "uniform samplerExternalOES sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
+ "}\n";
+
+ private float[] mMVPMatrix = new float[16];
+ private float[] mSTMatrix = new float[16];
+ private int mProgram;
+ private int mTextureID = -12345;
+ private int muMVPMatrixHandle;
+ private int muSTMatrixHandle;
+ private int maPositionHandle;
+ private int maTextureHandle;
+ private int rotationAngle = 0;
+
+ public TextureRenderer(int rotation) {
+ rotationAngle = rotation;
+ mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
+ mTriangleVertices.put(mTriangleVerticesData).position(0);
+ Matrix.setIdentityM(mSTMatrix, 0);
+ }
+
+ public int getTextureId() {
+ return mTextureID;
+ }
+
+ public void drawFrame(SurfaceTexture st, boolean invert) {
+ checkGlError("onDrawFrame start");
+ st.getTransformMatrix(mSTMatrix);
+
+ if (invert) {
+ mSTMatrix[5] = -mSTMatrix[5];
+ mSTMatrix[13] = 1.0f - mSTMatrix[13];
+ }
+
+ GLES20.glUseProgram(mProgram);
+ checkGlError("glUseProgram");
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);
+ mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
+ GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
+ checkGlError("glVertexAttribPointer maPosition");
+ GLES20.glEnableVertexAttribArray(maPositionHandle);
+ checkGlError("glEnableVertexAttribArray maPositionHandle");
+ mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
+ GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
+ checkGlError("glVertexAttribPointer maTextureHandle");
+ GLES20.glEnableVertexAttribArray(maTextureHandle);
+ checkGlError("glEnableVertexAttribArray maTextureHandle");
+ GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0);
+ GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ checkGlError("glDrawArrays");
+ GLES20.glFinish();
+ }
+
+ public void surfaceCreated() {
+ mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
+ if (mProgram == 0) {
+ throw new RuntimeException("failed creating program");
+ }
+ maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
+ checkGlError("glGetAttribLocation aPosition");
+ if (maPositionHandle == -1) {
+ throw new RuntimeException("Could not get attrib location for aPosition");
+ }
+ maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");
+ checkGlError("glGetAttribLocation aTextureCoord");
+ if (maTextureHandle == -1) {
+ throw new RuntimeException("Could not get attrib location for aTextureCoord");
+ }
+ muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
+ checkGlError("glGetUniformLocation uMVPMatrix");
+ if (muMVPMatrixHandle == -1) {
+ throw new RuntimeException("Could not get attrib location for uMVPMatrix");
+ }
+ muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix");
+ checkGlError("glGetUniformLocation uSTMatrix");
+ if (muSTMatrixHandle == -1) {
+ throw new RuntimeException("Could not get attrib location for uSTMatrix");
+ }
+ int[] textures = new int[1];
+ GLES20.glGenTextures(1, textures, 0);
+ mTextureID = textures[0];
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);
+ checkGlError("glBindTexture mTextureID");
+ GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError("glTexParameter");
+
+ Matrix.setIdentityM(mMVPMatrix, 0);
+ if (rotationAngle != 0) {
+ Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1);
+ }
+ }
+
+ public void changeFragmentShader(String fragmentShader) {
+ GLES20.glDeleteProgram(mProgram);
+ mProgram = createProgram(VERTEX_SHADER, fragmentShader);
+ if (mProgram == 0) {
+ throw new RuntimeException("failed creating program");
+ }
+ }
+
+ private int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ checkGlError("glCreateShader type=" + shaderType);
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ }
+ return shader;
+ }
+
+ private int createProgram(String vertexSource, String fragmentSource) {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+ if (vertexShader == 0) {
+ return 0;
+ }
+ int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+ if (pixelShader == 0) {
+ return 0;
+ }
+ int program = GLES20.glCreateProgram();
+ checkGlError("glCreateProgram");
+ if (program == 0) {
+ return 0;
+ }
+ GLES20.glAttachShader(program, vertexShader);
+ checkGlError("glAttachShader");
+ GLES20.glAttachShader(program, pixelShader);
+ checkGlError("glAttachShader");
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ }
+ return program;
+ }
+
+ public void checkGlError(String op) {
+ int error;
+ if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ throw new RuntimeException(op + ": glError " + error);
+ }
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/utils/video/Track.java b/src/main/java/de/pixart/messenger/utils/video/Track.java
new file mode 100644
index 000000000..347c65490
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/utils/video/Track.java
@@ -0,0 +1,263 @@
+/*
+ * This is the source code of Telegram for Android v. 1.7.x.
+ * It is licensed under GNU GPL v. 2 or later.
+ * You should have received a copy of the license in this archive (see LICENSE).
+ *
+ * Copyright Nikolai Kudashov, 2013-2014.
+ */
+
+package de.pixart.messenger.utils.video;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+
+import com.coremedia.iso.boxes.AbstractMediaHeaderBox;
+import com.coremedia.iso.boxes.SampleDescriptionBox;
+import com.coremedia.iso.boxes.SoundMediaHeaderBox;
+import com.coremedia.iso.boxes.VideoMediaHeaderBox;
+import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry;
+import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry;
+import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox;
+import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig;
+import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor;
+import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor;
+import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.SLConfigDescriptor;
+import com.mp4parser.iso14496.part15.AvcConfigurationBox;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+@TargetApi(16)
+public class Track {
+ private long trackId = 0;
+ private ArrayList<Sample> samples = new ArrayList<Sample>();
+ private long duration = 0;
+ private String handler;
+ private AbstractMediaHeaderBox headerBox = null;
+ private SampleDescriptionBox sampleDescriptionBox = null;
+ private LinkedList<Integer> syncSamples = null;
+ private int timeScale;
+ private Date creationTime = new Date();
+ private int height;
+ private int width;
+ private float volume = 0;
+ private ArrayList<Long> sampleDurations = new ArrayList<Long>();
+ private boolean isAudio = false;
+ private static Map<Integer, Integer> samplingFrequencyIndexMap = new HashMap<Integer, Integer>();
+ private long lastPresentationTimeUs = 0;
+ private boolean first = true;
+
+ static {
+ samplingFrequencyIndexMap.put(96000, 0x0);
+ samplingFrequencyIndexMap.put(88200, 0x1);
+ samplingFrequencyIndexMap.put(64000, 0x2);
+ samplingFrequencyIndexMap.put(48000, 0x3);
+ samplingFrequencyIndexMap.put(44100, 0x4);
+ samplingFrequencyIndexMap.put(32000, 0x5);
+ samplingFrequencyIndexMap.put(24000, 0x6);
+ samplingFrequencyIndexMap.put(22050, 0x7);
+ samplingFrequencyIndexMap.put(16000, 0x8);
+ samplingFrequencyIndexMap.put(12000, 0x9);
+ samplingFrequencyIndexMap.put(11025, 0xa);
+ samplingFrequencyIndexMap.put(8000, 0xb);
+ }
+
+ public Track(int id, MediaFormat format, boolean isAudio) throws Exception {
+ trackId = id;
+ if (!isAudio) {
+ sampleDurations.add((long)3015);
+ duration = 3015;
+ width = format.getInteger(MediaFormat.KEY_WIDTH);
+ height = format.getInteger(MediaFormat.KEY_HEIGHT);
+ timeScale = 90000;
+ syncSamples = new LinkedList<Integer>();
+ handler = "vide";
+ headerBox = new VideoMediaHeaderBox();
+ sampleDescriptionBox = new SampleDescriptionBox();
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ if (mime.equals("video/avc")) {
+ VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1");
+ visualSampleEntry.setDataReferenceIndex(1);
+ visualSampleEntry.setDepth(24);
+ visualSampleEntry.setFrameCount(1);
+ visualSampleEntry.setHorizresolution(72);
+ visualSampleEntry.setVertresolution(72);
+ visualSampleEntry.setWidth(width);
+ visualSampleEntry.setHeight(height);
+
+ AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox();
+
+ if (format.getByteBuffer("csd-0") != null) {
+ ArrayList<byte[]> spsArray = new ArrayList<byte[]>();
+ ByteBuffer spsBuff = format.getByteBuffer("csd-0");
+ spsBuff.position(4);
+ byte[] spsBytes = new byte[spsBuff.remaining()];
+ spsBuff.get(spsBytes);
+ spsArray.add(spsBytes);
+
+ ArrayList<byte[]> ppsArray = new ArrayList<byte[]>();
+ ByteBuffer ppsBuff = format.getByteBuffer("csd-1");
+ ppsBuff.position(4);
+ byte[] ppsBytes = new byte[ppsBuff.remaining()];
+ ppsBuff.get(ppsBytes);
+ ppsArray.add(ppsBytes);
+ avcConfigurationBox.setSequenceParameterSets(spsArray);
+ avcConfigurationBox.setPictureParameterSets(ppsArray);
+ }
+ //ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(spsBytes);
+ //SeqParameterSet seqParameterSet = SeqParameterSet.read(byteArrayInputStream);
+
+ avcConfigurationBox.setAvcLevelIndication(13);
+ avcConfigurationBox.setAvcProfileIndication(100);
+ avcConfigurationBox.setBitDepthLumaMinus8(-1);
+ avcConfigurationBox.setBitDepthChromaMinus8(-1);
+ avcConfigurationBox.setChromaFormat(-1);
+ avcConfigurationBox.setConfigurationVersion(1);
+ avcConfigurationBox.setLengthSizeMinusOne(3);
+ avcConfigurationBox.setProfileCompatibility(0);
+
+ visualSampleEntry.addBox(avcConfigurationBox);
+ sampleDescriptionBox.addBox(visualSampleEntry);
+ } else if (mime.equals("video/mp4v")) {
+ VisualSampleEntry visualSampleEntry = new VisualSampleEntry("mp4v");
+ visualSampleEntry.setDataReferenceIndex(1);
+ visualSampleEntry.setDepth(24);
+ visualSampleEntry.setFrameCount(1);
+ visualSampleEntry.setHorizresolution(72);
+ visualSampleEntry.setVertresolution(72);
+ visualSampleEntry.setWidth(width);
+ visualSampleEntry.setHeight(height);
+
+ sampleDescriptionBox.addBox(visualSampleEntry);
+ }
+ } else {
+ sampleDurations.add((long)1024);
+ duration = 1024;
+ isAudio = true;
+ volume = 1;
+ timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+ handler = "soun";
+ headerBox = new SoundMediaHeaderBox();
+ sampleDescriptionBox = new SampleDescriptionBox();
+ AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a");
+ audioSampleEntry.setChannelCount(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
+ audioSampleEntry.setSampleRate(format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
+ audioSampleEntry.setDataReferenceIndex(1);
+ audioSampleEntry.setSampleSize(16);
+
+ ESDescriptorBox esds = new ESDescriptorBox();
+ ESDescriptor descriptor = new ESDescriptor();
+ descriptor.setEsId(0);
+
+ SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor();
+ slConfigDescriptor.setPredefined(2);
+ descriptor.setSlConfigDescriptor(slConfigDescriptor);
+
+ DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor();
+ decoderConfigDescriptor.setObjectTypeIndication(0x40);
+ decoderConfigDescriptor.setStreamType(5);
+ decoderConfigDescriptor.setBufferSizeDB(1536);
+ decoderConfigDescriptor.setMaxBitRate(96000);
+ decoderConfigDescriptor.setAvgBitRate(96000);
+
+ AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig();
+ audioSpecificConfig.setAudioObjectType(2);
+ audioSpecificConfig.setSamplingFrequencyIndex(samplingFrequencyIndexMap.get((int)audioSampleEntry.getSampleRate()));
+ audioSpecificConfig.setChannelConfiguration(audioSampleEntry.getChannelCount());
+ decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig);
+
+ descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor);
+
+ ByteBuffer data = descriptor.serialize();
+ esds.setEsDescriptor(descriptor);
+ esds.setData(data);
+ audioSampleEntry.addBox(esds);
+ sampleDescriptionBox.addBox(audioSampleEntry);
+ }
+ }
+
+ public long getTrackId() {
+ return trackId;
+ }
+
+ public void addSample(long offset, MediaCodec.BufferInfo bufferInfo) {
+ boolean isSyncFrame = !isAudio && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ samples.add(new Sample(offset, bufferInfo.size));
+ if (syncSamples != null && isSyncFrame) {
+ syncSamples.add(samples.size());
+ }
+
+ long delta = bufferInfo.presentationTimeUs - lastPresentationTimeUs;
+ lastPresentationTimeUs = bufferInfo.presentationTimeUs;
+ delta = (delta * timeScale + 500000L) / 1000000L;
+ if (!first) {
+ sampleDurations.add(sampleDurations.size() - 1, delta);
+ duration += delta;
+ }
+ first = false;
+ }
+
+ public ArrayList<Sample> getSamples() {
+ return samples;
+ }
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public String getHandler() {
+ return handler;
+ }
+
+ public AbstractMediaHeaderBox getMediaHeaderBox() {
+ return headerBox;
+ }
+
+ public SampleDescriptionBox getSampleDescriptionBox() {
+ return sampleDescriptionBox;
+ }
+
+ public long[] getSyncSamples() {
+ if (syncSamples == null || syncSamples.isEmpty()) {
+ return null;
+ }
+ long[] returns = new long[syncSamples.size()];
+ for (int i = 0; i < syncSamples.size(); i++) {
+ returns[i] = syncSamples.get(i);
+ }
+ return returns;
+ }
+
+ public int getTimeScale() {
+ return timeScale;
+ }
+
+ public Date getCreationTime() {
+ return creationTime;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public float getVolume() {
+ return volume;
+ }
+
+ public ArrayList<Long> getSampleDurations() {
+ return sampleDurations;
+ }
+
+ public boolean isAudio() {
+ return isAudio;
+ }
+}