![JAR search and dependency download from the Maven repository](/logo.png)
org.mp4parser.streaming.output.mp4.StandardMp4Writer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of streaming Show documentation
Show all versions of streaming Show documentation
This package has a focus on streams. It can read A/V data from e.g. a network source.
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