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

org.mp4parser.streaming.output.mp4.StandardMp4Writer Maven / Gradle / Ivy

Go to download

This package has a focus on streams. It can read A/V data from e.g. a network source.

There is a newer version: 1.9.56
Show newest version
package org.mp4parser.streaming.output.mp4;

import org.mp4parser.Box;
import org.mp4parser.boxes.iso14496.part12.*;
import org.mp4parser.streaming.StreamingSample;
import org.mp4parser.streaming.StreamingTrack;
import org.mp4parser.streaming.extensions.CompositionTimeSampleExtension;
import org.mp4parser.streaming.extensions.CompositionTimeTrackExtension;
import org.mp4parser.streaming.extensions.SampleFlagsSampleExtension;
import org.mp4parser.streaming.extensions.TrackIdTrackExtension;
import org.mp4parser.streaming.output.SampleSink;
import org.mp4parser.tools.Mp4Arrays;
import org.mp4parser.tools.Mp4Math;
import org.mp4parser.tools.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

import static org.mp4parser.tools.CastUtils.l2i;

/**
 * Creates an MP4 file with ftyp, mdat+, moov order.
 * A very special property of this variant is that it written sequentially. You can start transferring the
 * data while the sink receives it. (in contrast to typical implementations which need random
 * access to write length fields at the beginning of the file)
 */
public class StandardMp4Writer extends DefaultBoxes implements SampleSink {

    public static final Object OBJ = new Object();
    private static Logger LOG = LoggerFactory.getLogger(FragmentedMp4Writer.class.getName());
    protected final WritableByteChannel sink;
    protected List source;
    protected Date creationTime = new Date();


    protected Map congestionControl = new ConcurrentHashMap();
    /**
     * Contains the start time of the next segment in line that will be created.
     */
    protected Map nextChunkCreateStartTime = new ConcurrentHashMap();
    /**
     * Contains the start time of the next segment in line that will be written.
     */
    protected Map nextChunkWriteStartTime = new ConcurrentHashMap();
    /**
     * Contains the next sample's start time.
     */
    protected Map nextSampleStartTime = new HashMap();
    /**
     * Buffers the samples per track until there are enough samples to form a Segment.
     */
    protected Map> sampleBuffers = new HashMap>();
    protected Map trackBoxes = new HashMap();
    /**
     * Buffers segements until it's time for a segment to be written.
     */
    protected Map> chunkBuffers = new ConcurrentHashMap>();
    protected Map chunkNumbers = new HashMap();
    protected Map sampleNumbers = new HashMap();
    long bytesWritten = 0;
    volatile boolean headerWritten = false;


    public StandardMp4Writer(List source, WritableByteChannel sink) {
        this.source = new ArrayList(source);
        this.sink = sink;

        HashSet trackIds = new HashSet();
        for (StreamingTrack streamingTrack : source) {
            streamingTrack.setSampleSink(this);
            chunkNumbers.put(streamingTrack, 1L);
            sampleNumbers.put(streamingTrack, 1L);
            nextSampleStartTime.put(streamingTrack, 0L);
            nextChunkCreateStartTime.put(streamingTrack, 0L);
            nextChunkWriteStartTime.put(streamingTrack, 0L);
            congestionControl.put(streamingTrack, new CountDownLatch(0));
            sampleBuffers.put(streamingTrack, new ArrayList());
            chunkBuffers.put(streamingTrack, new LinkedList());
            if (streamingTrack.getTrackExtension(TrackIdTrackExtension.class) != null) {
                TrackIdTrackExtension trackIdTrackExtension = streamingTrack.getTrackExtension(TrackIdTrackExtension.class);
                assert trackIdTrackExtension != null;
                if (trackIds.contains(trackIdTrackExtension.getTrackId())) {
                    throw new RuntimeException("There may not be two tracks with the same trackID within one file");
                }
            }

        }
        for (StreamingTrack streamingTrack : source) {
            if (streamingTrack.getTrackExtension(TrackIdTrackExtension.class) == null) {
                long maxTrackId = 0;
                for (Long trackId : trackIds) {
                    maxTrackId = Math.max(trackId, maxTrackId);
                }
                TrackIdTrackExtension tiExt = new TrackIdTrackExtension(maxTrackId + 1);
                trackIds.add(tiExt.getTrackId());
                streamingTrack.addTrackExtension(tiExt);
            }
        }
    }

    public void close() throws IOException {
        for (StreamingTrack streamingTrack : source) {
            writeChunkContainer(createChunkContainer(streamingTrack));
            streamingTrack.close();
        }
        write(sink, createMoov());
    }

    protected Box createMoov() {
        MovieBox movieBox = new MovieBox();

        movieBox.addBox(createMvhd());

        for (StreamingTrack streamingTrack : source) {
            movieBox.addBox(trackBoxes.get(streamingTrack));
        }

        // metadata here
        return movieBox;
    }

    private void sortTracks() {
        Collections.sort(source, new Comparator() {
            public int compare(StreamingTrack o1, StreamingTrack o2) {
                // compare times and account for timestamps!
                long a = nextChunkWriteStartTime.get(o1) * o2.getTimescale();
                long b = nextChunkWriteStartTime.get(o2) * o1.getTimescale();
                double d = Math.signum(a - b);
                return (int) d;
            }
        });
    }

    protected Box createMvhd() {
        MovieHeaderBox mvhd = new MovieHeaderBox();
        mvhd.setVersion(1);
        mvhd.setCreationTime(creationTime);
        mvhd.setModificationTime(creationTime);


        long[] timescales = new long[0];
        long maxTrackId = 0;
        double duration = 0;
        for (StreamingTrack streamingTrack : source) {
            duration = Math.max((double) nextSampleStartTime.get(streamingTrack) / streamingTrack.getTimescale(), duration);
            timescales = Mp4Arrays.copyOfAndAppend(timescales, streamingTrack.getTimescale());
            maxTrackId = Math.max(streamingTrack.getTrackExtension(TrackIdTrackExtension.class).getTrackId(), maxTrackId);
        }


        mvhd.setTimescale(Mp4Math.lcm(timescales));
        mvhd.setDuration((long) (Mp4Math.lcm(timescales) * duration));
        // find the next available trackId
        mvhd.setNextTrackId(maxTrackId + 1);
        return mvhd;
    }

    protected void write(WritableByteChannel out, Box... boxes) throws IOException {
        for (Box box1 : boxes) {
            box1.getBox(out);
            bytesWritten += box1.getSize();
        }
    }

    /**
     * Tests if the currently received samples for a given track
     * are already a 'chunk' as we want to have it. The next
     * sample will not be part of the chunk
     * will be added to the fragment buffer later.
     *
     * @param streamingTrack track to test
     * @param next           the lastest samples
     * @return true if a chunk is to b e created.
     */
    protected boolean isChunkReady(StreamingTrack streamingTrack, StreamingSample next) {
        long ts = nextSampleStartTime.get(streamingTrack);
        long cfst = nextChunkCreateStartTime.get(streamingTrack);

        return (ts >= cfst + 2 * streamingTrack.getTimescale());
        // chunk interleave of 2 seconds
    }

    protected void writeChunkContainer(ChunkContainer chunkContainer) throws IOException {
        TrackBox tb = trackBoxes.get(chunkContainer.streamingTrack);
        ChunkOffsetBox stco = Path.getPath(tb, "mdia[0]/minf[0]/stbl[0]/stco[0]");
        assert stco != null;
        stco.setChunkOffsets(Mp4Arrays.copyOfAndAppend(stco.getChunkOffsets(), bytesWritten + 8));
        write(sink, chunkContainer.mdat);
    }

    public void acceptSample(StreamingSample streamingSample, StreamingTrack streamingTrack) throws IOException {

        TrackBox tb = trackBoxes.get(streamingTrack);
        if (tb == null) {
            tb = new TrackBox();
            tb.addBox(createTkhd(streamingTrack));
            tb.addBox(createMdia(streamingTrack));
            trackBoxes.put(streamingTrack, tb);
        }

        // We might want to do that when the chunk is created to save memory copy


        synchronized (OBJ) {
            // need to synchronized here - I don't want two headers written under any circumstances
            if (!headerWritten) {
                boolean allTracksAtLeastOneSample = true;
                for (StreamingTrack track : source) {
                    allTracksAtLeastOneSample &= (nextSampleStartTime.get(track) > 0 || track == streamingTrack);
                }
                if (allTracksAtLeastOneSample) {
                    write(sink, createFtyp());
                    headerWritten = true;
                }
            }
        }

        try {
            CountDownLatch cdl = congestionControl.get(streamingTrack);
            if (cdl.getCount() > 0) {
                cdl.await();
            }
        } catch (InterruptedException e) {
            // don't care just move on
        }

        if (isChunkReady(streamingTrack, streamingSample)) {

            ChunkContainer chunkContainer = createChunkContainer(streamingTrack);
            //System.err.println("Creating fragment for " + streamingTrack);
            sampleBuffers.get(streamingTrack).clear();
            nextChunkCreateStartTime.put(streamingTrack, nextChunkCreateStartTime.get(streamingTrack) + chunkContainer.duration);
            Queue chunkQueue = chunkBuffers.get(streamingTrack);
            chunkQueue.add(chunkContainer);
            synchronized (OBJ) {
                if (headerWritten && this.source.get(0) == streamingTrack) {

                    Queue tracksFragmentQueue;
                    StreamingTrack currentStreamingTrack;
                    // This will write AT LEAST the currently created fragment and possibly a few more
                    while (!(tracksFragmentQueue = chunkBuffers.get(
                            (currentStreamingTrack = this.source.get(0))
                    )).isEmpty()) {
                        ChunkContainer currentFragmentContainer = tracksFragmentQueue.remove();
                        writeChunkContainer(currentFragmentContainer);
                        congestionControl.get(currentStreamingTrack).countDown();
                        long ts = nextChunkWriteStartTime.get(currentStreamingTrack) + currentFragmentContainer.duration;
                        nextChunkWriteStartTime.put(currentStreamingTrack, ts);
                        if (LOG.isTraceEnabled()) {
                            LOG.trace(currentStreamingTrack + " advanced to " + (double) ts / currentStreamingTrack.getTimescale());
                        }
                        sortTracks();
                    }
                } else {
                    if (chunkQueue.size() > 10) {
                        // if there are more than 10 fragments in the queue we don't want more samples of this track
                        // System.err.println("Stopping " + streamingTrack);
                        congestionControl.put(streamingTrack, new CountDownLatch(chunkQueue.size()));
                    }
                }
            }


        }


        sampleBuffers.get(streamingTrack).add(streamingSample);
        nextSampleStartTime.put(streamingTrack, nextSampleStartTime.get(streamingTrack) + streamingSample.getDuration());

    }

    private ChunkContainer createChunkContainer(StreamingTrack streamingTrack) {

        List samples = sampleBuffers.get(streamingTrack);
        long chunkNumber = chunkNumbers.get(streamingTrack);
        chunkNumbers.put(streamingTrack, chunkNumber + 1);
        ChunkContainer cc = new ChunkContainer();
        cc.streamingTrack = streamingTrack;
        cc.mdat = new Mdat(samples);
        cc.duration = nextSampleStartTime.get(streamingTrack) - nextChunkCreateStartTime.get(streamingTrack);
        TrackBox tb = trackBoxes.get(streamingTrack);
        SampleTableBox stbl = Path.getPath(tb, "mdia[0]/minf[0]/stbl[0]");
        assert stbl != null;
        SampleToChunkBox stsc = Path.getPath(stbl, "stsc[0]");
        assert stsc != null;
        if (stsc.getEntries().isEmpty()) {
            List entries = new ArrayList();
            stsc.setEntries(entries);
            entries.add(new SampleToChunkBox.Entry(chunkNumber, samples.size(), 1));
        } else {
            SampleToChunkBox.Entry e = stsc.getEntries().get(stsc.getEntries().size() - 1);
            if (e.getSamplesPerChunk() != samples.size()) {
                stsc.getEntries().add(new SampleToChunkBox.Entry(chunkNumber, samples.size(), 1));
            }
        }
        long sampleNumber = sampleNumbers.get(streamingTrack);

        SampleSizeBox stsz = Path.getPath(stbl, "stsz[0]");
        TimeToSampleBox stts = Path.getPath(stbl, "stts[0]");
        SyncSampleBox stss = Path.getPath(stbl, "stss[0]");
        CompositionTimeToSample ctts = Path.getPath(stbl, "ctts[0]");
        if (streamingTrack.getTrackExtension(CompositionTimeTrackExtension.class) != null) {
            if (ctts == null) {
                ctts = new CompositionTimeToSample();
                ctts.setEntries(new ArrayList());

                ArrayList bs = new ArrayList(stbl.getBoxes());
                bs.add(bs.indexOf(stts), ctts);
            }
        }

        long[] sampleSizes = new long[samples.size()];
        int i = 0;
        for (StreamingSample sample : samples) {
            sampleSizes[i++] = sample.getContent().limit();

            if (ctts != null) {
                ctts.getEntries().add(
                        new CompositionTimeToSample.Entry(1, l2i(sample.getSampleExtension(CompositionTimeSampleExtension.class).getCompositionTimeOffset())));
            }

            assert stts != null;
            if (stts.getEntries().isEmpty()) {
                ArrayList entries = new ArrayList(stts.getEntries());
                entries.add(new TimeToSampleBox.Entry(1, sample.getDuration()));
                stts.setEntries(entries);
            } else {
                TimeToSampleBox.Entry sttsEntry = stts.getEntries().get(stts.getEntries().size() - 1);
                if (sttsEntry.getDelta() == sample.getDuration()) {
                    sttsEntry.setCount(sttsEntry.getCount() + 1);
                } else {
                    stts.getEntries().add(new TimeToSampleBox.Entry(1, sample.getDuration()));
                }
            }
            SampleFlagsSampleExtension sampleFlagsSampleExtension = sample.getSampleExtension(SampleFlagsSampleExtension.class);
            if (sampleFlagsSampleExtension != null && sampleFlagsSampleExtension.isSyncSample()) {
                if (stss == null) {
                    stss = new SyncSampleBox();
                    stbl.addBox(stss);
                }
                stss.setSampleNumber(Mp4Arrays.copyOfAndAppend(stss.getSampleNumber(), sampleNumber));
            }
            sampleNumber++;

        }
        assert stsz != null;
        stsz.setSampleSizes(Mp4Arrays.copyOfAndAppend(stsz.getSampleSizes(), sampleSizes));

        sampleNumbers.put(streamingTrack, sampleNumber);
        samples.clear();
        LOG.debug("CC created. mdat size: " + cc.mdat.size);
        return cc;
    }

    protected Box createMdhd(StreamingTrack streamingTrack) {
        MediaHeaderBox mdhd = new MediaHeaderBox();
        mdhd.setCreationTime(creationTime);
        mdhd.setModificationTime(creationTime);
        mdhd.setDuration(nextSampleStartTime.get(streamingTrack));
        mdhd.setTimescale(streamingTrack.getTimescale());
        mdhd.setLanguage(streamingTrack.getLanguage());
        return mdhd;
    }

    private class Mdat implements Box {
        ArrayList samples;
        long size;

        public Mdat(List samples) {
            this.samples = new ArrayList(samples);
            size = 8;
            for (StreamingSample sample : samples) {
                size += sample.getContent().limit();
            }
        }

        public String getType() {
            return "mdat";
        }

        public long getSize() {
            return size;
        }

        public void getBox(WritableByteChannel writableByteChannel) throws IOException {
            writableByteChannel.write(ByteBuffer.wrap(new byte[]{
                    (byte) ((size & 0xff000000) >> 24),
                    (byte) ((size & 0xff0000) >> 16),
                    (byte) ((size & 0xff00) >> 8),
                    (byte) ((size & 0xff)),
                    109, 100, 97, 116, // mdat

            }));
            for (StreamingSample sample : samples) {
                writableByteChannel.write((ByteBuffer) sample.getContent().rewind());
            }


        }
    }

    private class ChunkContainer {
        Mdat mdat;
        StreamingTrack streamingTrack;
        long duration;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy