package org.mp4parser.streaming.input.mp4; import org.mp4parser.BasicContainer; import org.mp4parser.Box; import org.mp4parser.BoxParser; import org.mp4parser.PropertyBoxParserImpl; import org.mp4parser.boxes.iso14496.part12.*; import org.mp4parser.streaming.StreamingSample; import org.mp4parser.streaming.StreamingTrack; import org.mp4parser.streaming.TrackExtension; 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.input.StreamingSampleImpl; import org.mp4parser.streaming.output.SampleSink; import org.mp4parser.streaming.output.mp4.FragmentedMp4Writer; import org.mp4parser.tools.Path; import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.*; import java.util.concurrent.Callable; import static org.mp4parser.tools.CastUtils.l2i; /** * Creates a List of StreamingTrack from a classic MP4. Fragmented MP4s don't * work and the implementation will consume a lot of heap when the MP4 * is not a 'fast-start' MP4 (order: ftyp, moov, mdat good; * order ftyp, mdat, moov bad). */ // @todo implement FragmentedMp4ContainerSource // @todo store mdat of non-fast-start MP4 on disk public class ClassicMp4ContainerSource implements Callable<Void> { final HashMap<TrackBox, Mp4StreamingTrack> tracks = new LinkedHashMap<TrackBox, Mp4StreamingTrack>(); final HashMap<TrackBox, Long> currentChunks = new HashMap<TrackBox, Long>(); final HashMap<TrackBox, Long> currentSamples = new HashMap<TrackBox, Long>(); final DiscardingByteArrayOutputStream baos = new DiscardingByteArrayOutputStream(); final ReadableByteChannel readableByteChannel; private final ByteBuffer BUFFER = ByteBuffer.allocateDirect(65535); public ClassicMp4ContainerSource(InputStream is) throws IOException { readableByteChannel = Channels.newChannel(new TeeInputStream(is, baos)); BasicContainer container = new BasicContainer(); BoxParser boxParser = new PropertyBoxParserImpl(); Box current = null; while (current == null || !"moov".equals(current.getType())) { current = boxParser.parseBox(readableByteChannel, null); container.addBox(current); } // Either mdat was already read (yeahh sucks but what can you do if it's in the beginning) // or it's still coming for (TrackBox trackBox : Path.<TrackBox>getPaths(container, "moov[0]/trak")) { Mp4StreamingTrack mp4StreamingTrack = new Mp4StreamingTrack(trackBox); tracks.put(trackBox, mp4StreamingTrack); if (trackBox.getSampleTableBox().getCompositionTimeToSample() != null) { mp4StreamingTrack.addTrackExtension(new CompositionTimeTrackExtension()); } mp4StreamingTrack.addTrackExtension(new TrackIdTrackExtension(trackBox.getTrackHeaderBox().getTrackId())); currentChunks.put(trackBox, 1L); currentSamples.put(trackBox, 1L); } } public static void main(String[] args) throws IOException { ClassicMp4ContainerSource classicMp4ContainerSource = null; try { classicMp4ContainerSource = new ClassicMp4ContainerSource(new URI("http://org.mp4parser.s3.amazonaws.com/examples/Cosmos%20Laundromat%20small%20faststart.mp4").toURL().openStream()); } catch (URISyntaxException e) { throw new IOException(e); } List<StreamingTrack> streamingTracks = classicMp4ContainerSource.getTracks(); File f = new File("output.mp4"); FragmentedMp4Writer writer = new FragmentedMp4Writer(streamingTracks, new FileOutputStream(f).getChannel()); System.out.println("Reading and writing started."); classicMp4ContainerSource.call(); writer.close(); System.err.println(f.getAbsolutePath()); } List<StreamingTrack> getTracks() { return new ArrayList<StreamingTrack>(tracks.values()); } public Void call() throws IOException { while (true) { TrackBox firstInLine = null; long currentChunk = 0; long currentChunkStartSample = 0; long offset = Long.MAX_VALUE; SampleToChunkBox.Entry entry = null; for (TrackBox trackBox : tracks.keySet()) { long _currentChunk = currentChunks.get(trackBox); long _currentSample = currentSamples.get(trackBox); long[] chunkOffsets = trackBox.getSampleTableBox().getChunkOffsetBox().getChunkOffsets(); if ((l2i(_currentChunk) - 1 < chunkOffsets.length) && chunkOffsets[l2i(_currentChunk) - 1] < offset) { firstInLine = trackBox; currentChunk = _currentChunk; currentChunkStartSample = _currentSample; offset = chunkOffsets[l2i(_currentChunk) - 1]; } } if (firstInLine == null) { break; } SampleToChunkBox stsc = firstInLine.getSampleTableBox().getSampleToChunkBox(); for (SampleToChunkBox.Entry _entry : stsc.getEntries()) { if (currentChunk >= _entry.getFirstChunk()) { entry = _entry; } else { break; } } assert entry != null; SampleTableBox stbl = firstInLine.getSampleTableBox(); List<TimeToSampleBox.Entry> times = stbl.getTimeToSampleBox().getEntries(); List<CompositionTimeToSample.Entry> compositionOffsets = stbl.getCompositionTimeToSample() != null ? stbl.getCompositionTimeToSample().getEntries() : null; //System.out.println(trackId + ": Pushing chunk with sample " + currentChunkStartSample + "(offset: " + offset + ") to " + (currentChunkStartSample + entry.getSamplesPerChunk()) + " in the chunk"); SampleSizeBox stsz = stbl.getSampleSizeBox(); for (long index = currentChunkStartSample; index < currentChunkStartSample + entry.getSamplesPerChunk(); index++) { final long duration = times.get(0).getDelta(); if (times.get(0).getCount() == 1) { times.remove(0); } else { times.get(0).setCount(times.get(0).getCount() - 1); } // Sample Flags Start SampleDependencyTypeBox sdtp = Path.getPath(stbl, "sdtp"); SampleFlagsSampleExtension sfse = new SampleFlagsSampleExtension(); if (sdtp != null) { SampleDependencyTypeBox.Entry e = sdtp.getEntries().get(l2i(index)); sfse.setIsLeading(e.getIsLeading()); sfse.setSampleDependsOn(e.getSampleDependsOn()); sfse.setSampleIsDependedOn(e.getSampleIsDependedOn()); sfse.setSampleHasRedundancy(e.getSampleHasRedundancy()); } if (stbl.getSyncSampleBox() != null) { if (Arrays.binarySearch(stbl.getSyncSampleBox().getSampleNumber(), index) >= 0) { sfse.setSampleIsNonSyncSample(false); } else { sfse.setSampleIsNonSyncSample(true); } } DegradationPriorityBox stdp = Path.getPath(stbl, "stdp"); if (stdp != null) { sfse.setSampleDegradationPriority(stdp.getPriorities()[l2i(index)]); } // Sample Flags Done int sampleSize = l2i(stsz.getSampleSizeAtIndex(l2i(index - 1))); long avail = baos.available(); // as long as the sample has not yet been fully read // read more bytes from the input channel to fill // while (avail <= offset + sampleSize) { try { int br = readableByteChannel.read(BUFFER); if (br == -1) { break; } avail = baos.available(); BUFFER.rewind(); } catch (IOException e) { throw new RuntimeException(e); } } //System.err.println("Get sample content @" + offset + " len=" + sampleSize); final byte[] sampleContent = baos.get(offset, sampleSize); StreamingSample streamingSample = new StreamingSampleImpl(sampleContent, duration); streamingSample.addSampleExtension(sfse); if (compositionOffsets != null && !compositionOffsets.isEmpty()) { final long compositionOffset = compositionOffsets.get(0).getOffset(); if (compositionOffsets.get(0).getCount() == 1) { compositionOffsets.remove(0); } else { compositionOffsets.get(0).setCount(compositionOffsets.get(0).getCount() - 1); } streamingSample.addSampleExtension(CompositionTimeSampleExtension.create(compositionOffset)); } if (firstInLine.getTrackHeaderBox().getTrackId() == 1) { System.out.println("Pushing sample @" + offset + " of " + sampleSize + " bytes (i=" + index + ")"); } tracks.get(firstInLine).getSampleSink().acceptSample(streamingSample, tracks.get(firstInLine)); offset += sampleSize; } baos.discardTo(offset); currentChunks.put(firstInLine, currentChunk + 1); currentSamples.put(firstInLine, currentChunkStartSample + entry.getSamplesPerChunk()); } for (Mp4StreamingTrack mp4StreamingTrack : tracks.values()) { mp4StreamingTrack.close(); } System.out.println("All Samples read."); return null; } public static class Mp4StreamingTrack implements StreamingTrack { private final TrackBox trackBox; protected HashMap<Class<? extends TrackExtension>, TrackExtension> trackExtensions = new HashMap<Class<? extends TrackExtension>, TrackExtension>(); boolean allSamplesRead = false; SampleSink sampleSink; public Mp4StreamingTrack(TrackBox trackBox) { this.trackBox = trackBox; } public void close() { allSamplesRead = true; } public boolean isClosed() { return allSamplesRead; } public long getTimescale() { return trackBox.getMediaBox().getMediaHeaderBox().getTimescale(); } public SampleSink getSampleSink() { return sampleSink; } public void setSampleSink(SampleSink sampleSink) { this.sampleSink = sampleSink; } public String getHandler() { return trackBox.getMediaBox().getHandlerBox().getHandlerType(); } public String getLanguage() { return trackBox.getMediaBox().getMediaHeaderBox().getLanguage(); } public SampleDescriptionBox getSampleDescriptionBox() { return trackBox.getSampleTableBox().getSampleDescriptionBox(); } public <T extends TrackExtension> T getTrackExtension(Class<T> clazz) { return (T) trackExtensions.get(clazz); } public void addTrackExtension(TrackExtension trackExtension) { trackExtensions.put(trackExtension.getClass(), trackExtension); } public void removeTrackExtension(Class<? extends TrackExtension> clazz) { trackExtensions.remove(clazz); } } public static class TeeInputStream extends FilterInputStream { /** * The output stream that will receive a copy of all bytes read from the * proxied input stream. */ private final OutputStream branch; long counter = 0; /** * Creates a TeeInputStream that proxies the given {@link InputStream} * and copies all read bytes to the given {@link OutputStream}. The given * output stream will not be closed when this stream gets closed. * * @param input input stream to be proxied * @param branch output stream that will receive a copy of all bytes read */ public TeeInputStream(InputStream input, OutputStream branch) { super(input); this.branch = branch; } /** * Reads a single byte from the proxied input stream and writes it to * the associated output stream. * * @return next byte from the stream, or -1 if the stream has ended * @throws IOException if the stream could not be read (or written) */ @Override public int read() throws IOException { int ch = super.read(); if (ch != -1) { branch.write(ch); counter++; } return ch; } /** * Reads bytes from the proxied input stream and writes the read bytes * to the associated output stream. * * @param bts byte buffer * @param st start offset within the buffer * @param end maximum number of bytes to read * @return number of bytes read, or -1 if the stream has ended * @throws IOException if the stream could not be read (or written) */ @Override public int read(byte[] bts, int st, int end) throws IOException { int n = super.read(bts, st, end); if (n != -1) { branch.write(bts, st, n); counter += n; } return n; } /** * Reads bytes from the proxied input stream and writes the read bytes * to the associated output stream. * * @param bts byte buffer * @return number of bytes read, or -1 if the stream has ended * @throws IOException if the stream could not be read (or written) */ @Override public int read(byte[] bts) throws IOException { int n = super.read(bts); if (n != -1) { branch.write(bts, 0, n); counter += n; } return n; } } }