com.mp4parser.streaming.MultiTrackFragmentedMp4Writer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of isoparser Show documentation
Show all versions of isoparser Show documentation
A generic parser and writer for all ISO 14496 based files (MP4, Quicktime, DCF, PDCF, ...)
package com.mp4parser.streaming;
import com.coremedia.iso.IsoFile;
import com.coremedia.iso.IsoTypeWriter;
import com.coremedia.iso.boxes.Box;
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.HintMediaHeaderBox;
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.NullMediaHeaderBox;
import com.coremedia.iso.boxes.SampleSizeBox;
import com.coremedia.iso.boxes.SampleTableBox;
import com.coremedia.iso.boxes.SampleToChunkBox;
import com.coremedia.iso.boxes.SoundMediaHeaderBox;
import com.coremedia.iso.boxes.StaticChunkOffsetBox;
import com.coremedia.iso.boxes.SubtitleMediaHeaderBox;
import com.coremedia.iso.boxes.TimeToSampleBox;
import com.coremedia.iso.boxes.TrackBox;
import com.coremedia.iso.boxes.VideoMediaHeaderBox;
import com.coremedia.iso.boxes.fragment.MovieExtendsBox;
import com.coremedia.iso.boxes.fragment.MovieExtendsHeaderBox;
import com.coremedia.iso.boxes.fragment.MovieFragmentBox;
import com.coremedia.iso.boxes.fragment.MovieFragmentHeaderBox;
import com.coremedia.iso.boxes.fragment.SampleFlags;
import com.coremedia.iso.boxes.fragment.TrackExtendsBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentBaseMediaDecodeTimeBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentHeaderBox;
import com.coremedia.iso.boxes.fragment.TrackRunBox;
import com.googlecode.mp4parser.util.Mp4Arrays;
import com.mp4parser.streaming.extensions.CencEncryptTrackExtension;
import com.mp4parser.streaming.extensions.CompositionTimeSampleExtension;
import com.mp4parser.streaming.extensions.CompositionTimeTrackExtension;
import com.mp4parser.streaming.extensions.SampleFlagsSampleExtension;
import com.mp4parser.streaming.extensions.SampleFlagsTrackExtension;
import com.mp4parser.streaming.extensions.TrackIdTrackExtension;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.googlecode.mp4parser.util.Math.lcm;
import static com.mp4parser.streaming.StreamingSampleHelper.getSampleExtension;
/**
*
*/
public class MultiTrackFragmentedMp4Writer implements StreamingMp4Writer {
private final OutputStream outputStream;
StreamingTrack source[];
CompositionTimeTrackExtension compositionTimeTrackExtension;
SampleFlagsTrackExtension sampleDependencyTrackExtension;
Date creationTime;
Map> fragmentBuffers = new HashMap>();
private long sequenceNumber = 1;
private long currentFragmentStartTime = 0;
private long currentTime = 0;
public MultiTrackFragmentedMp4Writer(StreamingTrack[] source, OutputStream outputStream) {
this.source = source;
this.outputStream = outputStream;
this.creationTime = new Date();
HashSet trackIds = new HashSet();
for (StreamingTrack streamingTrack : source) {
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) {
ArrayList ts = new ArrayList(trackIds);
Collections.sort(ts);
streamingTrack.addTrackExtension(new TrackIdTrackExtension(ts.size() > 0 ? (ts.get(ts.size() - 1) + 1) : 1));
}
}
}
public void close() {
}
protected Box createMvhd() {
MovieHeaderBox mvhd = new MovieHeaderBox();
mvhd.setVersion(1);
mvhd.setCreationTime(creationTime);
mvhd.setModificationTime(creationTime);
mvhd.setDuration(0);//no duration in moov for fragmented movies
long[] timescales = new long[0];
for (StreamingTrack streamingTrack : source) {
Mp4Arrays.copyOfAndAppend(timescales, streamingTrack.getTimescale());
}
mvhd.setTimescale(lcm(timescales));
// find the next available trackId
mvhd.setNextTrackId(2);
return mvhd;
}
protected Box createMdiaHdlr(StreamingTrack streamingTrack) {
HandlerBox hdlr = new HandlerBox();
hdlr.setHandlerType(streamingTrack.getHandler());
return hdlr;
}
protected Box createMdhd(StreamingTrack streamingTrack) {
MediaHeaderBox mdhd = new MediaHeaderBox();
mdhd.setCreationTime(creationTime);
mdhd.setModificationTime(creationTime);
mdhd.setDuration(0);//no duration in moov for fragmented movies
mdhd.setTimescale(streamingTrack.getTimescale());
mdhd.setLanguage(streamingTrack.getLanguage());
return mdhd;
}
protected Box createMdia(StreamingTrack streamingTrack) {
MediaBox mdia = new MediaBox();
mdia.addBox(createMdhd(streamingTrack));
mdia.addBox(createMdiaHdlr(streamingTrack));
mdia.addBox(createMinf(streamingTrack));
return mdia;
}
protected Box createMinf(StreamingTrack streamingTrack) {
MediaInformationBox minf = new MediaInformationBox();
if (streamingTrack.getHandler().equals("vide")) {
minf.addBox(new VideoMediaHeaderBox());
} else if (streamingTrack.getHandler().equals("soun")) {
minf.addBox(new SoundMediaHeaderBox());
} else if (streamingTrack.getHandler().equals("text")) {
minf.addBox(new NullMediaHeaderBox());
} else if (streamingTrack.getHandler().equals("subt")) {
minf.addBox(new SubtitleMediaHeaderBox());
} else if (streamingTrack.getHandler().equals("hint")) {
minf.addBox(new HintMediaHeaderBox());
} else if (streamingTrack.getHandler().equals("sbtl")) {
minf.addBox(new NullMediaHeaderBox());
}
minf.addBox(createDinf());
minf.addBox(createStbl(streamingTrack));
return minf;
}
protected Box createStbl(StreamingTrack streamingTrack) {
SampleTableBox stbl = new SampleTableBox();
stbl.addBox(streamingTrack.getSampleDescriptionBox());
stbl.addBox(new TimeToSampleBox());
stbl.addBox(new SampleToChunkBox());
stbl.addBox(new SampleSizeBox());
stbl.addBox(new StaticChunkOffsetBox());
return stbl;
}
protected DataInformationBox createDinf() {
DataInformationBox dinf = new DataInformationBox();
DataReferenceBox dref = new DataReferenceBox();
dinf.addBox(dref);
DataEntryUrlBox url = new DataEntryUrlBox();
url.setFlags(1);
dref.addBox(url);
return dinf;
}
protected Box createTrak(StreamingTrack streamingTrack) {
TrackBox trackBox = new TrackBox();
trackBox.addBox(streamingTrack.getTrackHeaderBox());
trackBox.addBox(streamingTrack.getTrackHeaderBox());
trackBox.addBox(createMdia(streamingTrack));
return trackBox;
}
public Box createFtyp() {
List minorBrands = new LinkedList();
minorBrands.add("isom");
minorBrands.add("iso6");
minorBrands.add("avc1");
return new FileTypeBox("isom", 0, minorBrands);
}
protected Box createMvex() {
MovieExtendsBox mvex = new MovieExtendsBox();
final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox();
mved.setVersion(1);
mved.setFragmentDuration(0);
mvex.addBox(mved);
for (StreamingTrack streamingTrack : source) {
mvex.addBox(createTrex(streamingTrack));
}
return mvex;
}
protected Box createTrex(StreamingTrack streamingTrack) {
TrackExtendsBox trex = new TrackExtendsBox();
trex.setTrackId(streamingTrack.getTrackHeaderBox().getTrackId());
trex.setDefaultSampleDescriptionIndex(1);
trex.setDefaultSampleDuration(0);
trex.setDefaultSampleSize(0);
SampleFlags sf = new SampleFlags();
if ("soun".equals(streamingTrack.getHandler()) || "subt".equals(streamingTrack.getHandler())) {
// as far as I know there is no audio encoding
// where the sample are not self contained.
// same seems to be true for subtitle tracks
sf.setSampleDependsOn(2);
sf.setSampleIsDependedOn(2);
}
trex.setDefaultSampleFlags(sf);
return trex;
}
protected Box createMoov() {
MovieBox movieBox = new MovieBox();
movieBox.addBox(createMvhd());
for (StreamingTrack streamingTrack : source) {
movieBox.addBox(createTrak(streamingTrack));
;
}
movieBox.addBox(createMvex());
// metadata here
return movieBox;
}
class ConsumeSamplesCallable implements Callable {
private StreamingTrack streamingTrack;
public ConsumeSamplesCallable(StreamingTrack streamingTrack) {
this.streamingTrack = streamingTrack;
}
public Object call() throws Exception {
do {
try {
StreamingSample ss;
while ((ss = streamingTrack.getSamples().poll(100, TimeUnit.MILLISECONDS)) != null) {
consumeSample(streamingTrack, ss);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (streamingTrack.hasMoreSamples());
return null;
}
}
public void write() throws IOException {
final WritableByteChannel out = Channels.newChannel(outputStream);
createFtyp().getBox(out);
createMoov().getBox(out);
ExecutorService es = Executors.newFixedThreadPool(source.length);
for (StreamingTrack streamingTrack : source) {
es.submit(new ConsumeSamplesCallable(streamingTrack));
}
}
private synchronized void consumeSample(StreamingTrack streamingTrack, StreamingSample ss) throws IOException {
SampleFlagsSampleExtension sampleDependencySampleExtension = null;
CompositionTimeSampleExtension compositionTimeSampleExtension = null;
for (SampleExtension sampleExtension : ss.getExtensions()) {
if (sampleExtension instanceof SampleFlagsSampleExtension) {
sampleDependencySampleExtension = (SampleFlagsSampleExtension) sampleExtension;
} else if (sampleExtension instanceof CompositionTimeSampleExtension) {
compositionTimeSampleExtension = (CompositionTimeSampleExtension) sampleExtension;
}
}
currentTime += ss.getDuration();
// 3 seconds = 3 * source.getTimescale()
fragmentBuffers.get(streamingTrack).add(ss);
if (currentTime > currentFragmentStartTime + 3 * streamingTrack.getTimescale() &&
fragmentBuffers.size() > 0 &&
(sampleDependencyTrackExtension == null ||
sampleDependencySampleExtension == null ||
sampleDependencySampleExtension.isSyncSample())) {
WritableByteChannel out = Channels.newChannel(outputStream);
createMoof(streamingTrack).getBox(out);
createMdat(streamingTrack).getBox(out);
currentFragmentStartTime = currentTime;
fragmentBuffers.clear();
}
}
private Box createMoof(StreamingTrack streamingTrack) {
MovieFragmentBox moof = new MovieFragmentBox();
createMfhd(sequenceNumber, moof);
createTraf(streamingTrack, moof);
TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0);
firstTrun.setDataOffset(1); // dummy to make size correct
firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size
sequenceNumber++;
return moof;
}
protected void createTfhd(StreamingTrack streamingTrack, TrackFragmentBox parent) {
TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox();
SampleFlags sf = new SampleFlags();
tfhd.setDefaultSampleFlags(sf);
tfhd.setBaseDataOffset(-1);
tfhd.setTrackId(streamingTrack.getTrackExtension(TrackIdTrackExtension.class).getTrackId());
tfhd.setDefaultBaseIsMoof(true);
parent.addBox(tfhd);
}
protected void createTfdt(TrackFragmentBox parent) {
TrackFragmentBaseMediaDecodeTimeBox tfdt = new TrackFragmentBaseMediaDecodeTimeBox();
tfdt.setVersion(1);
tfdt.setBaseMediaDecodeTime(currentFragmentStartTime);
parent.addBox(tfdt);
}
protected void createTrun(StreamingTrack streamingTrack, TrackFragmentBox parent) {
TrackRunBox trun = new TrackRunBox();
trun.setVersion(1);
trun.setSampleDurationPresent(true);
trun.setSampleSizePresent(true);
List entries = new ArrayList(fragmentBuffers.size());
trun.setSampleCompositionTimeOffsetPresent(streamingTrack.getTrackExtension(CompositionTimeTrackExtension.class) != null);
boolean sampleFlagsRequired = streamingTrack.getTrackExtension(SampleFlagsTrackExtension.class) != null;
trun.setSampleFlagsPresent(sampleFlagsRequired);
for (StreamingSample streamingSample : fragmentBuffers.get(streamingTrack)) {
TrackRunBox.Entry entry = new TrackRunBox.Entry();
entry.setSampleSize(streamingSample.getContent().remaining());
if (sampleFlagsRequired) {
SampleFlagsSampleExtension sampleFlagsSampleExtension =
getSampleExtension(streamingSample, SampleFlagsSampleExtension.class);
assert sampleFlagsSampleExtension != null : "SampleDependencySampleExtension missing even though SampleDependencyTrackExtension was present";
SampleFlags sflags = new SampleFlags();
sflags.setIsLeading(sampleFlagsSampleExtension.getIsLeading());
sflags.setSampleIsDependedOn(sampleFlagsSampleExtension.getSampleIsDependedOn());
sflags.setSampleDependsOn(sampleFlagsSampleExtension.getSampleDependsOn());
sflags.setSampleHasRedundancy(sampleFlagsSampleExtension.getSampleHasRedundancy());
sflags.setSampleIsDifferenceSample(sampleFlagsSampleExtension.isSampleIsNonSyncSample());
sflags.setSamplePaddingValue(sampleFlagsSampleExtension.getSamplePaddingValue());
sflags.setSampleDegradationPriority(sampleFlagsSampleExtension.getSampleDegradationPriority());
entry.setSampleFlags(sflags);
}
entry.setSampleDuration(streamingSample.getDuration());
if (trun.isSampleCompositionTimeOffsetPresent()) {
CompositionTimeSampleExtension compositionTimeSampleExtension =
getSampleExtension(streamingSample, CompositionTimeSampleExtension.class);
assert compositionTimeSampleExtension != null : "CompositionTimeSampleExtension missing even though CompositionTimeTrackExtension was present";
entry.setSampleCompositionTimeOffset(compositionTimeSampleExtension.getCompositionTimeOffset());
}
entries.add(entry);
}
trun.setEntries(entries);
parent.addBox(trun);
}
private void createTraf(StreamingTrack streamingTrack, MovieFragmentBox moof) {
TrackFragmentBox traf = new TrackFragmentBox();
moof.addBox(traf);
createTfhd(streamingTrack, traf);
createTfdt(traf);
createTrun(streamingTrack, traf);
if (streamingTrack.getTrackExtension(CencEncryptTrackExtension.class) != null) {
// createSaiz(getTrackExtension(source, CencEncryptTrackExtension.class), sequenceNumber, traf);
// createSenc(getTrackExtension(source, CencEncryptTrackExtension.class), sequenceNumber, traf);
// createSaio(getTrackExtension(source, CencEncryptTrackExtension.class), sequenceNumber, traf);
}
/* Map> groupEntryFamilies = new HashMap>();
for (Map.Entry sg : track.getSampleGroups().entrySet()) {
String type = sg.getKey().getType();
List groupEntries = groupEntryFamilies.get(type);
if (groupEntries == null) {
groupEntries = new ArrayList();
groupEntryFamilies.put(type, groupEntries);
}
groupEntries.add(sg.getKey());
}
for (Map.Entry> sg : groupEntryFamilies.entrySet()) {
SampleGroupDescriptionBox sgpd = new SampleGroupDescriptionBox();
String type = sg.getKey();
sgpd.setGroupEntries(sg.getValue());
SampleToGroupBox sbgp = new SampleToGroupBox();
sbgp.setGroupingType(type);
SampleToGroupBox.Entry last = null;
for (int i = l2i(startSample - 1); i < l2i(endSample - 1); i++) {
int index = 0;
for (int j = 0; j < sg.getValue().size(); j++) {
GroupEntry groupEntry = sg.getValue().get(j);
long[] sampleNums = track.getSampleGroups().get(groupEntry);
if (Arrays.binarySearch(sampleNums, i) >= 0) {
index = j + 1;
}
}
if (last == null || last.getGroupDescriptionIndex() != index) {
last = new SampleToGroupBox.Entry(1, index);
sbgp.getEntries().add(last);
} else {
last.setSampleCount(last.getSampleCount() + 1);
}
}
traf.addBox(sgpd);
traf.addBox(sbgp);
}*/
}
private void createMfhd(long sequenceNumber, MovieFragmentBox moof) {
MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox();
mfhd.setSequenceNumber(sequenceNumber);
moof.addBox(mfhd);
}
private Box createMdat(final StreamingTrack streamingTrack) {
return new WriteOnlyBox("mdat") {
public long getSize() {
long l = 8;
for (StreamingSample streamingSample : fragmentBuffers.get(streamingTrack)) {
l += streamingSample.getContent().remaining();
}
return l;
}
public void getBox(WritableByteChannel writableByteChannel) throws IOException {
ArrayList sampleContents = new ArrayList();
long l = 8;
for (StreamingSample streamingSample : fragmentBuffers.get(streamingTrack)) {
ByteBuffer sampleContent = streamingSample.getContent();
sampleContents.add(sampleContent);
l += sampleContent.remaining();
}
ByteBuffer bb = ByteBuffer.allocate(8);
IsoTypeWriter.writeUInt32(bb, l);
bb.put(IsoFile.fourCCtoBytes(getType()));
writableByteChannel.write((ByteBuffer) bb.rewind());
for (ByteBuffer sampleContent : sampleContents) {
writableByteChannel.write(sampleContent);
}
}
};
}
}