All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jcodec.containers.mp4.MP4Demuxer Maven / Gradle / Ivy

There is a newer version: 0.2.5
Show newest version
package org.jcodec.containers.mp4;

import static org.jcodec.containers.mp4.QTTimeUtil.mediaToEdited;
import static org.jcodec.containers.mp4.boxes.Box.findFirst;
import gnu.trove.list.array.TIntArrayList;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.jcodec.common.DemuxerTrack;
import org.jcodec.common.NIOUtils;
import org.jcodec.common.SeekableByteChannel;
import org.jcodec.common.model.Packet;
import org.jcodec.common.model.RationalLarge;
import org.jcodec.common.model.TapeTimecode;
import org.jcodec.containers.mp4.boxes.AudioSampleEntry;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox.Entry;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.EditListBox;
import org.jcodec.containers.mp4.boxes.HandlerBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NameBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.SampleEntry;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry;
import org.jcodec.containers.mp4.boxes.SyncSamplesBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry;
import org.jcodec.containers.mp4.boxes.TimecodeSampleEntry;
import org.jcodec.containers.mp4.boxes.TrakBox;

/**
 * This class is part of JCodec ( www.jcodec.org ) This software is distributed
 * under FreeBSD License
 * 
 * Demuxer frontend for MP4
 * 
 * 
 * @author The JCodec project
 * 
 */
public class MP4Demuxer {

    private List tracks;
    private TimecodeTrack timecodeTrack;
    private SeekableByteChannel input;
    private MovieBox movie;

    public abstract class MP4DemuxerTrack implements DemuxerTrack {
        protected TrakBox box;
        private TrackType type;
        private int no;
        protected SampleEntry[] sampleEntries;

        protected TimeToSampleEntry[] timeToSamples;
        protected SampleToChunkEntry[] sampleToChunks;
        protected long[] chunkOffsets;

        protected long duration;

        protected int sttsInd;
        protected int sttsSubInd;

        protected int stcoInd;

        protected int stscInd;

        protected long pts;
        protected long curFrame;
        protected int timescale;

        public MP4DemuxerTrack(TrakBox trak) {
            no = trak.getTrackHeader().getNo();
            type = getTrackType(trak);
            sampleEntries = Box.findAll(trak, SampleEntry.class, "mdia", "minf", "stbl", "stsd", null);

            NodeBox stbl = trak.getMdia().getMinf().getStbl();

            TimeToSampleBox stts = findFirst(stbl, TimeToSampleBox.class, "stts");
            SampleToChunkBox stsc = findFirst(stbl, SampleToChunkBox.class, "stsc");
            ChunkOffsetsBox stco = findFirst(stbl, ChunkOffsetsBox.class, "stco");
            ChunkOffsets64Box co64 = findFirst(stbl, ChunkOffsets64Box.class, "co64");

            timeToSamples = stts.getEntries();
            sampleToChunks = stsc.getSampleToChunk();
            chunkOffsets = stco != null ? stco.getChunkOffsets() : co64.getChunkOffsets();

            for (TimeToSampleEntry ttse : timeToSamples) {
                duration += ttse.getSampleCount() * ttse.getSampleDuration();
            }
            box = trak;

            timescale = trak.getTimescale();
        }

        public int pts2Sample(long _tv, int _timescale) {
            long tv = _tv * timescale / _timescale;
            int ttsInd, sample = 0;
            for (ttsInd = 0; ttsInd < timeToSamples.length - 1; ttsInd++) {
                int a = timeToSamples[ttsInd].getSampleCount() * timeToSamples[ttsInd].getSampleDuration();
                if (tv < a)
                    break;
                tv -= a;
                sample += timeToSamples[ttsInd].getSampleCount();
            }
            return sample + (int) (tv / timeToSamples[ttsInd].getSampleDuration());
        }

        public TrackType getType() {
            return type;
        }

        public int getNo() {
            return no;
        }

        public SampleEntry[] getSampleEntries() {
            return sampleEntries;
        }

        public TrakBox getBox() {
            return box;
        }

        public long getTimescale() {
            return timescale;
        }

        public abstract MP4Packet getFrames(ByteBuffer buffer, int n) throws IOException;

        protected abstract void seekPointer(long frameNo);

        public boolean canSeek(long pts) {
            return pts >= 0 && pts < duration;
        }

        public synchronized boolean seek(long pts) {
            if (pts < 0)
                throw new IllegalArgumentException("Seeking to negative pts");
            if (pts >= duration)
                return false;

            sttsInd = 0;
            long prevDur = 0;
            int frameNo = 0;
            while (pts > prevDur + timeToSamples[sttsInd].getSampleCount() * timeToSamples[sttsInd].getSampleDuration()) {
                prevDur += timeToSamples[sttsInd].getSampleCount() * timeToSamples[sttsInd].getSampleDuration();
                frameNo += timeToSamples[sttsInd].getSampleCount();
            }
            sttsSubInd = (int) ((pts - prevDur) / timeToSamples[sttsInd].getSampleDuration());
            frameNo += sttsSubInd;
            this.pts = prevDur + timeToSamples[sttsInd].getSampleDuration() * sttsSubInd;

            seekPointer(frameNo);

            return true;
        }

        protected long shiftPts(long frames) {
            long result = 0;
            int rem;
            while (frames > (rem = timeToSamples[sttsInd].getSampleCount() - sttsSubInd)) {
                frames -= rem;
                result += rem * timeToSamples[sttsInd].getSampleDuration();
                sttsInd++;
                sttsSubInd = 0;
                if (sttsInd >= timeToSamples.length)
                    return result;
            }
            result += frames * timeToSamples[sttsInd].getSampleDuration();

            pts += result;

            return result;
        }

        protected void nextChunk() {
            if (stcoInd >= chunkOffsets.length)
                return;
            stcoInd++;

            if ((stscInd + 1 < sampleToChunks.length) && stcoInd + 1 == sampleToChunks[stscInd + 1].getFirst()) {
                stscInd++;
            }
        }

        public synchronized boolean gotoFrame(long frameNo) {
            if (frameNo < 0)
                throw new IllegalArgumentException("negative frame number");
            if (frameNo >= getFrameCount())
                return false;
            if (frameNo == curFrame)
                return true;

            seekPointer(frameNo);
            seekPts(frameNo);

            return true;
        }

        @Override
        public void seek(double second) {
            seek((long)second * timescale);
        }

        private void seekPts(long frameNo) {
            pts = sttsInd = sttsSubInd = 0;
            shiftPts(frameNo);
        }

        public int getStartTimecode() throws IOException {
            if (!box.isTimecode())
                throw new IllegalStateException("Not a timecode track");
            MP4Packet nextFrame = getFrames(ByteBuffer.allocate(4), 1);
            gotoFrame(0);
            return nextFrame.getData().duplicate().getInt();
        }

        public int parseTimecode(String tc) {
            String[] split = tc.split(":");

            TimecodeSampleEntry tmcd = Box.findFirst(box, TimecodeSampleEntry.class, "mdia", "minf", "stbl", "stsd",
                    "tmcd");
            byte nf = tmcd.getNumFrames();

            return Integer.parseInt(split[3]) + Integer.parseInt(split[2]) * nf + Integer.parseInt(split[1]) * 60 * nf
                    + Integer.parseInt(split[0]) * 3600 * nf;
        }

        public RationalLarge getDuration() {
            return new RationalLarge(box.getMediaDuration(), box.getTimescale());
        }

        public abstract long getFrameCount();

        public long getCurFrame() {
            return curFrame;
        }

        public List getEdits() {
            EditListBox editListBox = Box.findFirst(box, EditListBox.class, "edts", "elst");
            if (editListBox != null)
                return editListBox.getEdits();
            return null;
        }

        public String getName() {
            NameBox nameBox = Box.findFirst(box, NameBox.class, "udta", "name");
            return nameBox != null ? nameBox.getName() : null;
        }

        public String getFourcc() {
            return getSampleEntries()[0].getFourcc();
        }
    }

    public MP4DemuxerTrack create(TrakBox trak) {
        SampleSizesBox stsz = findFirst(trak, SampleSizesBox.class, "mdia", "minf", "stbl", "stsz");
        if (stsz.getDefaultSize() == 0)
            return new FramesTrack(trak);
        else
            return new SamplesTrack(trak);
    }

    /**
     * Track of audio samples
     * 
     */
    private class SamplesTrack extends MP4DemuxerTrack {

        private int defaultSampleSize;

        private long posShift;

        protected int totalFrames;

        public SamplesTrack(TrakBox trak) {
            super(trak);

            SampleSizesBox stsz = findFirst(trak, SampleSizesBox.class, "mdia", "minf", "stbl", "stsz");
            defaultSampleSize = stsz.getDefaultSize();

            int chunks = 0;
            for (int i = 1; i < sampleToChunks.length; i++) {
                int ch = (int) (sampleToChunks[i].getFirst() - sampleToChunks[i - 1].getFirst());
                totalFrames += ch * sampleToChunks[i - 1].getCount();
                chunks += ch;
            }
            totalFrames += sampleToChunks[sampleToChunks.length - 1].getCount() * (chunkOffsets.length - chunks);
        }

        public Packet getFrames(int n) throws IOException {
            return getFrames(ByteBuffer.allocate(n * getFrameSize()), n);
        }

        public synchronized MP4Packet getFrames(ByteBuffer _result, int n) throws IOException {
            if (n < 0)
                throw new IllegalArgumentException("Negative number of frames");

            int frameSize = getFrameSize();
            n = (int) (curFrame + n > totalFrames ? totalFrames - curFrame : n);
            if (n == 0 || stcoInd >= chunkOffsets.length) {
                return null;
            }

            ByteBuffer result = _result.duplicate();
            int tgtLen = frameSize * n;
            if (tgtLen > result.remaining()) {
                throw new IllegalArgumentException("Insufficient room to fit " + n + " samples");
            }

            int se = sampleToChunks[stscInd].getEntry();
            do {
                long chSize = sampleToChunks[stscInd].getCount() * frameSize;
                int toRead = (int) Math.min(result.remaining(), chSize - posShift);
                int read;
                synchronized (input) {
                    input.position(chunkOffsets[stcoInd] + posShift);
                    read = NIOUtils.read(input, result, toRead);
                }
                if (read == -1)
                    break;
                posShift += read;

                if (posShift == chSize) {
                    nextChunk();
                    posShift = 0;
                }
            } while (result.hasRemaining() && stcoInd < chunkOffsets.length && sampleToChunks[stscInd].getEntry() == se);

            long _pts = pts;
            int doneFrames = (tgtLen - result.remaining()) / frameSize;
            shiftPts(doneFrames);

            result.flip();

            MP4Packet pkt = new MP4Packet(result, QTTimeUtil.mediaToEdited(box, _pts, movie.getTimescale()), timescale,
                    (int) (pts - _pts), curFrame, true, null, _pts, se - 1);

            curFrame += doneFrames;

            return pkt;
        }

        public int getFrameSize() {
            SampleEntry entry = sampleEntries[sampleToChunks[stscInd].getEntry() - 1];
            if (entry instanceof AudioSampleEntry) {
                return ((AudioSampleEntry) entry).calcFrameSize();
            } else {
                return defaultSampleSize;
            }
        }

        protected void seekPointer(long frameNo) {
            for (stcoInd = 0, stscInd = 0, curFrame = 0;;) {
                long nextFrame = curFrame + sampleToChunks[stscInd].getCount();
                if (nextFrame > frameNo)
                    break;
                curFrame = nextFrame;
                nextChunk();
            }
            posShift = (int) ((frameNo - curFrame) * getFrameSize());
            curFrame = frameNo;
        }

        public long getFrameCount() {
            return totalFrames;
        }
    }

    /**
     * Timecode track, provides timecode information for video track
     * 
     */
    public class TimecodeTrack {

        private TrakBox box;
        private TimeToSampleEntry[] timeToSamples;
        private int[] samples;
        private TimecodeSampleEntry tse;

        public TimecodeTrack(TrakBox trak) throws IOException {
            this.box = trak;

            NodeBox stbl = trak.getMdia().getMinf().getStbl();

            TimeToSampleBox stts = findFirst(stbl, TimeToSampleBox.class, "stts");
            SampleToChunkBox stsc = findFirst(stbl, SampleToChunkBox.class, "stsc");
            ChunkOffsetsBox stco = findFirst(stbl, ChunkOffsetsBox.class, "stco");
            ChunkOffsets64Box co64 = findFirst(stbl, ChunkOffsets64Box.class, "co64");

            timeToSamples = stts.getEntries();
            readSamples(stsc.getSampleToChunk(), stco != null ? stco.getChunkOffsets() : co64.getChunkOffsets());

            tse = (TimecodeSampleEntry) box.getSampleEntries()[0];
        }

        public MP4Packet getTimecode(MP4Packet pkt) {

            long tv = QTTimeUtil
                    .editedToMedia(box, box.rescale(pkt.getPts(), pkt.getTimescale()), movie.getTimescale());
            int sample;
            int ttsInd = 0, ttsSubInd = 0;
            for (sample = 0; sample < samples.length - 1; sample++) {
                int dur = timeToSamples[ttsInd].getSampleDuration();
                if (tv < dur)
                    break;
                tv -= dur;
                ttsSubInd++;
                if (ttsInd < timeToSamples.length - 1 && ttsSubInd >= timeToSamples[ttsInd].getSampleCount())
                    ttsInd++;
            }

            int frameNo = (int) ((((2 * tv * tse.getTimescale()) / box.getTimescale()) / tse.getFrameDuration()) + 1) / 2;

            return new MP4Packet(pkt, getTimecode(samples[sample], frameNo, tse));
        }

        private TapeTimecode getTimecode(int startCounter, int frameNo, TimecodeSampleEntry entry) {
            int frame = dropFrameAdjust(frameNo + startCounter, entry);
            int sec = frame / entry.getNumFrames();
            return new TapeTimecode((short) (sec / 3600), (byte) ((sec / 60) % 60), (byte) (sec % 60),
                    (byte) (frame % entry.getNumFrames()), entry.isDropFrame());
        }

        private int dropFrameAdjust(int frame, TimecodeSampleEntry entry) {
            if (entry.isDropFrame()) {
                long D = frame / 17982;
                long M = frame % 17982;
                frame += 18 * D + 2 * ((M - 2) / 1798);
            }
            return frame;
        }

        private void readSamples(SampleToChunkEntry[] sampleToChunks, long[] chunkOffsets) throws IOException {
            synchronized (input) {
                int stscInd = 0;
                TIntArrayList ss = new TIntArrayList();
                for (int chunkNo = 0; chunkNo < chunkOffsets.length; chunkNo++) {
                    int nSamples = sampleToChunks[stscInd].getCount();
                    if (stscInd < sampleToChunks.length - 1 && chunkNo + 1 >= sampleToChunks[stscInd + 1].getFirst())
                        stscInd++;
                    long offset = chunkOffsets[chunkNo];
                    input.position(offset);
                    ByteBuffer buf = NIOUtils.fetchFrom(input, nSamples * 4);
                    for (int i = 0; i < nSamples; i++) {
                        ss.add(buf.getInt());
                    }
                }
                samples = ss.toArray();
            }
        }

        /**
         * 
         * @return
         * @deprecated Use getTimecode to automatically populate tape timecode
         *             for each frame
         */
        public int getStartTimecode() {
            return samples[0];
        }
    }

    /**
     * Track of video frames
     * 
     */
    public class FramesTrack extends MP4DemuxerTrack {

        private int[] sizes;

        private long offInChunk;

        private int noInChunk;

        private int[] syncSamples;
        private int ssOff;

        private Entry[] compOffsets;
        private int cttsInd;
        private int cttsSubInd;

        public FramesTrack(TrakBox trak) {
            super(trak);

            SampleSizesBox stsz = findFirst(trak, SampleSizesBox.class, "mdia", "minf", "stbl", "stsz");
            SyncSamplesBox stss = Box.findFirst(trak, SyncSamplesBox.class, "mdia", "minf", "stbl", "stss");
            CompositionOffsetsBox ctts = Box.findFirst(trak, CompositionOffsetsBox.class, "mdia", "minf", "stbl",
                    "ctts");
            compOffsets = ctts == null ? null : ctts.getEntries();
            if (stss != null) {
                syncSamples = stss.getSyncSamples();
            }
            sizes = stsz.getSizes();
        }

        public synchronized MP4Packet getFrames(int n) throws IOException {
            if (n != 1)
                throw new IllegalArgumentException("Frames should be = 1 for this track");
            if (curFrame >= sizes.length)
                return null;
            int size = sizes[(int) curFrame];

            return getFrames(ByteBuffer.allocate(size), 1);
        }

        public synchronized MP4Packet getFrames(ByteBuffer _result, int n) throws IOException {
            if (n != 1)
                throw new IllegalArgumentException("Frames should be = 1 for this track");

            if (curFrame >= sizes.length)
                return null;
            int size = sizes[(int) curFrame];

            if (_result.remaining() < size) {
                throw new IllegalArgumentException("Buffer size is not enough to fit a packet");
            }
            ByteBuffer result = _result.duplicate();
            result.limit(size);

            synchronized (input) {
                input.position(chunkOffsets[stcoInd] + offInChunk);
                if (NIOUtils.read(input, result) < size)
                    return null;
            }

            int duration = timeToSamples[sttsInd].getSampleDuration();

            boolean sync = syncSamples == null;

            if (syncSamples != null && ssOff < syncSamples.length && (curFrame + 1) == syncSamples[ssOff]) {
                sync = true;
                ssOff++;
            }

            long realPts = pts;
            if (compOffsets != null) {
                realPts = pts + compOffsets[cttsInd].getOffset();
                cttsSubInd++;
                if (cttsInd < compOffsets.length - 1 && cttsSubInd == compOffsets[cttsInd].getCount()) {
                    cttsInd++;
                    cttsSubInd = 0;
                }
            }

            result.flip();

            MP4Packet pkt = new MP4Packet(result, mediaToEdited(box, realPts, movie.getTimescale()), timescale,
                    duration, curFrame, sync, null, realPts, sampleToChunks[stscInd].getEntry() - 1);

            offInChunk += size;

            curFrame++;
            noInChunk++;
            if (noInChunk >= sampleToChunks[stscInd].getCount()) {
                noInChunk = 0;
                offInChunk = 0;

                nextChunk();
            }
            shiftPts(1);
            sttsSubInd++;

            return pkt;
        }

        protected void seekPointer(long frameNo) {
            if (compOffsets != null) {
                cttsSubInd = (int) frameNo;
                cttsInd = 0;
                while (cttsSubInd >= compOffsets[cttsInd].getCount()) {
                    cttsSubInd -= compOffsets[cttsInd].getCount();
                    cttsInd++;
                }
            }

            curFrame = (int) frameNo;
            stcoInd = 0;
            stscInd = 0;
            noInChunk = (int) frameNo;
            offInChunk = 0;

            while (noInChunk >= sampleToChunks[stscInd].getCount()) {
                noInChunk -= sampleToChunks[stscInd].getCount();

                nextChunk();
            }

            for (int i = 0; i < noInChunk; i++) {
                offInChunk += sizes[(int) frameNo - noInChunk + i];
            }

            if (syncSamples != null)
                for (ssOff = 0; syncSamples[ssOff] < curFrame + 1; ssOff++)
                    ;

        }

        public long getFrameCount() {
            return sizes.length;
        }
    };

    public MP4Demuxer(SeekableByteChannel input) throws IOException {
        this.input = input;
        tracks = new LinkedList();
        findMovieBox(input);
    }

    public MP4DemuxerTrack[] getTracks() {
        return tracks.toArray(new MP4DemuxerTrack[] {});
    }

    private void findMovieBox(SeekableByteChannel input) throws IOException {
        movie = MP4Util.parseMovie(input);
        if (movie == null)
            throw new IOException("Could not find movie meta information box");

        processHeader(movie);
    }

    private void processHeader(NodeBox moov) throws IOException {
        TrakBox tt = null;
        for (TrakBox trak : Box.findAll(moov, TrakBox.class, "trak")) {
            SampleEntry se = Box.findFirst(trak, SampleEntry.class, "mdia", "minf", "stbl", "stsd", null);
            if ("tmcd".equals(se.getFourcc())) {
                tt = trak;
            } else {
                tracks.add(create(trak));
            }
        }
        if (tt != null) {
            MP4DemuxerTrack video = getVideoTrack();
            if (video != null)
                timecodeTrack = new TimecodeTrack(tt);
        }
    }

    public static TrackType getTrackType(TrakBox trak) {
        HandlerBox handler = findFirst(trak, HandlerBox.class, "mdia", "hdlr");
        return TrackType.fromHandler(handler.getComponentSubType());
    }

    public MP4DemuxerTrack getVideoTrack() {
        for (MP4DemuxerTrack demuxerTrack : tracks) {
            if (demuxerTrack.box.isVideo())
                return demuxerTrack;
        }
        return null;
    }

    public MovieBox getMovie() {
        return movie;
    }

    public MP4DemuxerTrack getTrack(int no) {
        for (MP4DemuxerTrack track : tracks) {
            if (track.getNo() == no)
                return track;
        }
        return null;
    }

    public List getAudioTracks() {
        ArrayList result = new ArrayList();
        for (MP4DemuxerTrack demuxerTrack : tracks) {
            if (demuxerTrack.box.isAudio())
                result.add(demuxerTrack);
        }
        return result;
    }

    public TimecodeTrack getTimecodeTrack() {
        return timecodeTrack;
    }

    private static int ftyp = ('f' << 24) | ('t' << 16) | ('y' << 8) | 'p';
    private static int free = ('f' << 24) | ('r' << 16) | ('e' << 8) | 'e';
    private static int moov = ('m' << 24) | ('o' << 16) | ('o' << 8) | 'v';
    private static int mdat = ('m' << 24) | ('d' << 16) | ('a' << 8) | 't';

    public static int probe(final ByteBuffer b) {
        ByteBuffer fork = b.duplicate();
        int success = 0;
        int total = 0;
        while (fork.remaining() >= 8) {
            long len = fork.getInt();
            int fcc = fork.getInt();
            int hdrLen = 8;
            if (len == 1) {
                len = fork.getLong();
                hdrLen = 16;
            } else if (len < 8)
                break;
            if (fcc == ftyp && len < 64 || fcc == moov && len < 100 * 1024 * 1024 || fcc == free || fcc == mdat)
                success++;
            total++;
            if (len >= Integer.MAX_VALUE)
                break;
            NIOUtils.skip(fork, (int) (len - hdrLen));
        }

        return success * 100 / total;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy