/* * 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 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 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 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 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()); 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 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); } }