diff options
Diffstat (limited to 'isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming')
16 files changed, 2469 insertions, 0 deletions
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/all-wcprops new file mode 100644 index 0000000..7d70c40 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/all-wcprops @@ -0,0 +1,47 @@ +K 25 +svn:wc:ra_dav:version-url +V 100 +/svn/!svn/ver/773/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming +END +VideoQuality.java +K 25 +svn:wc:ra_dav:version-url +V 118 +/svn/!svn/ver/760/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/VideoQuality.java +END +FlatPackageWriterImpl.java +K 25 +svn:wc:ra_dav:version-url +V 127 +/svn/!svn/ver/760/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatPackageWriterImpl.java +END +ManifestWriter.java +K 25 +svn:wc:ra_dav:version-url +V 120 +/svn/!svn/ver/755/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/ManifestWriter.java +END +AbstractManifestWriter.java +K 25 +svn:wc:ra_dav:version-url +V 128 +/svn/!svn/ver/757/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AbstractManifestWriter.java +END +PackageWriter.java +K 25 +svn:wc:ra_dav:version-url +V 119 +/svn/!svn/ver/755/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/PackageWriter.java +END +AudioQuality.java +K 25 +svn:wc:ra_dav:version-url +V 118 +/svn/!svn/ver/760/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AudioQuality.java +END +FlatManifestWriterImpl.java +K 25 +svn:wc:ra_dav:version-url +V 128 +/svn/!svn/ver/773/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatManifestWriterImpl.java +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/entries new file mode 100644 index 0000000..619b17c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/entries @@ -0,0 +1,266 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming +http://mp4parser.googlecode.com/svn + + + +2012-09-01T21:55:19.768646Z +773 +michael.stattmann@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +VideoQuality.java +file + + + + +2012-09-14T17:27:50.317216Z +356fcadf80f684d83b5f30afd5cb26e4 +2012-08-17T15:20:10.783404Z +760 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +807 + +FlatPackageWriterImpl.java +file + + + + +2012-09-14T17:27:50.317216Z +f38a8b91e1b8abd48e1ae26b23b060fa +2012-08-17T15:20:10.783404Z +760 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +8285 + +ManifestWriter.java +file + + + + +2012-09-14T17:27:50.317216Z +4fc006c7919c1ab4ed498340dfa133b3 +2012-08-17T01:13:17.213046Z +755 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +992 + +AbstractManifestWriter.java +file + + + + +2012-09-14T17:27:50.317216Z +1ce766c781ae825fb0620a61eb2b2e1c +2012-08-17T05:55:12.215481Z +757 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +5030 + +PackageWriter.java +file + + + + +2012-09-14T17:27:50.317216Z +ffdb02efc14eeadf6c1ba9c5e500e76c +2012-08-17T01:13:17.213046Z +755 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +878 + +AudioQuality.java +file + + + + +2012-09-14T17:27:50.317216Z +c2b5ada192ff228aac261452067773fd +2012-08-17T15:20:10.783404Z +760 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +887 + +FlatManifestWriterImpl.java +file + + + + +2012-09-14T17:27:50.317216Z +d45a45107db5f4c43765d95708382310 +2012-09-01T21:55:19.768646Z +773 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +30095 + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AbstractManifestWriter.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AbstractManifestWriter.java.svn-base new file mode 100644 index 0000000..6ee4ffa --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AbstractManifestWriter.java.svn-base @@ -0,0 +1,126 @@ +package com.googlecode.mp4parser.authoring.adaptivestreaming;
+
+import com.coremedia.iso.boxes.OriginalFormatBox;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.coremedia.iso.boxes.sampleentry.SampleEntry;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static com.googlecode.mp4parser.util.CastUtils.l2i;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: mstattma
+ * Date: 17.08.12
+ * Time: 02:51
+ * To change this template use File | Settings | File Templates.
+ */
+public abstract class AbstractManifestWriter implements ManifestWriter {
+ private static final Logger LOG = Logger.getLogger(AbstractManifestWriter.class.getName());
+
+ private FragmentIntersectionFinder intersectionFinder;
+ protected long[] audioFragmentsDurations;
+ protected long[] videoFragmentsDurations;
+
+ protected AbstractManifestWriter(FragmentIntersectionFinder intersectionFinder) {
+ this.intersectionFinder = intersectionFinder;
+ }
+
+ /**
+ * Calculates the length of each fragment in the given <code>track</code> (as part of <code>movie</code>).
+ *
+ * @param track target of calculation
+ * @param movie the <code>track</code> must be part of this <code>movie</code>
+ * @return the duration of each fragment in track timescale
+ */
+ public long[] calculateFragmentDurations(Track track, Movie movie) {
+ long[] startSamples = intersectionFinder.sampleNumbers(track, movie);
+ long[] durations = new long[startSamples.length];
+ int currentFragment = 0;
+ int currentSample = 1; // sync samples start with 1 !
+
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ for (int max = currentSample + l2i(entry.getCount()); currentSample < max; currentSample++) {
+ // in this loop we go through the entry.getCount() samples starting from current sample.
+ // the next entry.getCount() samples have the same decoding time.
+ if (currentFragment != startSamples.length - 1 && currentSample == startSamples[currentFragment + 1]) {
+ // we are not in the last fragment && the current sample is the start sample of the next fragment
+ currentFragment++;
+ }
+ durations[currentFragment] += entry.getDelta();
+
+
+ }
+ }
+ return durations;
+
+ }
+
+ public long getBitrate(Track track) {
+ long bitrate = 0;
+ for (ByteBuffer sample : track.getSamples()) {
+ bitrate += sample.limit();
+ }
+ bitrate *= 8; // from bytes to bits
+ bitrate /= ((double) getDuration(track)) / track.getTrackMetaData().getTimescale(); // per second
+ return bitrate;
+ }
+
+ protected static long getDuration(Track track) {
+ long duration = 0;
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ duration += entry.getCount() * entry.getDelta();
+ }
+ return duration;
+ }
+
+ protected long[] checkFragmentsAlign(long[] referenceTimes, long[] checkTimes) throws IOException {
+
+ if (referenceTimes == null || referenceTimes.length == 0) {
+ return checkTimes;
+ }
+ long[] referenceTimesMinusLast = new long[referenceTimes.length - 1];
+ System.arraycopy(referenceTimes, 0, referenceTimesMinusLast, 0, referenceTimes.length - 1);
+ long[] checkTimesMinusLast = new long[checkTimes.length - 1];
+ System.arraycopy(checkTimes, 0, checkTimesMinusLast, 0, checkTimes.length - 1);
+
+ if (!Arrays.equals(checkTimesMinusLast, referenceTimesMinusLast)) {
+ String log = "";
+ log += (referenceTimes.length);
+ log += ("Reference : [");
+ for (long l : referenceTimes) {
+ log += (String.format("%10d,", l));
+ }
+ log += ("]");
+ LOG.warning(log);
+ log = "";
+
+ log += (checkTimes.length);
+ log += ("Current : [");
+ for (long l : checkTimes) {
+ log += (String.format("%10d,", l));
+ }
+ log += ("]");
+ LOG.warning(log);
+ throw new IOException("Track does not have the same fragment borders as its predecessor.");
+
+ } else {
+ return checkTimes;
+ }
+ }
+
+ protected String getFormat(SampleEntry se) {
+ String type = se.getType();
+ if (type.equals("encv") || type.equals("enca") || type.equals("encv")) {
+ OriginalFormatBox frma = se.getBoxes(OriginalFormatBox.class, true).get(0);
+ type = frma.getDataFormat();
+ }
+ return type;
+ }
+}
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AudioQuality.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AudioQuality.java.svn-base new file mode 100644 index 0000000..39e115f --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/AudioQuality.java.svn-base @@ -0,0 +1,29 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + + +public class AudioQuality { + String fourCC; + long bitrate; + int audioTag; + long samplingRate; + int channels; + int bitPerSample; + int packetSize; + String language; + String codecPrivateData; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatManifestWriterImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatManifestWriterImpl.java.svn-base new file mode 100644 index 0000000..5cc9be9 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatManifestWriterImpl.java.svn-base @@ -0,0 +1,643 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.coremedia.iso.Hex; +import com.coremedia.iso.boxes.SampleDescriptionBox; +import com.coremedia.iso.boxes.SoundMediaHeaderBox; +import com.coremedia.iso.boxes.VideoMediaHeaderBox; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.Version; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder; +import com.googlecode.mp4parser.boxes.DTSSpecificBox; +import com.googlecode.mp4parser.boxes.EC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +public class FlatManifestWriterImpl extends AbstractManifestWriter { + private static final Logger LOG = Logger.getLogger(FlatManifestWriterImpl.class.getName()); + + protected FlatManifestWriterImpl(FragmentIntersectionFinder intersectionFinder) { + super(intersectionFinder); + } + + /** + * Overwrite this method in subclasses to add your specialities. + * + * @param manifest the original manifest + * @return your customized version of the manifest + */ + protected Document customizeManifest(Document manifest) { + return manifest; + } + + public String getManifest(Movie movie) throws IOException { + + LinkedList<VideoQuality> videoQualities = new LinkedList<VideoQuality>(); + long videoTimescale = -1; + + LinkedList<AudioQuality> audioQualities = new LinkedList<AudioQuality>(); + long audioTimescale = -1; + + for (Track track : movie.getTracks()) { + if (track.getMediaHeaderBox() instanceof VideoMediaHeaderBox) { + videoFragmentsDurations = checkFragmentsAlign(videoFragmentsDurations, calculateFragmentDurations(track, movie)); + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + videoQualities.add(getVideoQuality(track, (VisualSampleEntry) stsd.getSampleEntry())); + if (videoTimescale == -1) { + videoTimescale = track.getTrackMetaData().getTimescale(); + } else { + assert videoTimescale == track.getTrackMetaData().getTimescale(); + } + } + if (track.getMediaHeaderBox() instanceof SoundMediaHeaderBox) { + audioFragmentsDurations = checkFragmentsAlign(audioFragmentsDurations, calculateFragmentDurations(track, movie)); + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + audioQualities.add(getAudioQuality(track, (AudioSampleEntry) stsd.getSampleEntry())); + if (audioTimescale == -1) { + audioTimescale = track.getTrackMetaData().getTimescale(); + } else { + assert audioTimescale == track.getTrackMetaData().getTimescale(); + } + + } + } + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException(e); + } + Document document = documentBuilder.newDocument(); + + + Element smoothStreamingMedia = document.createElement("SmoothStreamingMedia"); + document.appendChild(smoothStreamingMedia); + smoothStreamingMedia.setAttribute("MajorVersion", "2"); + smoothStreamingMedia.setAttribute("MinorVersion", "1"); +// silverlight ignores the timescale attr smoothStreamingMedia.addAttribute(new Attribute("TimeScale", Long.toString(movieTimeScale))); + smoothStreamingMedia.setAttribute("Duration", "0"); + + smoothStreamingMedia.appendChild(document.createComment(Version.VERSION)); + Element videoStreamIndex = document.createElement("StreamIndex"); + videoStreamIndex.setAttribute("Type", "video"); + videoStreamIndex.setAttribute("TimeScale", Long.toString(videoTimescale)); // silverlight ignores the timescale attr + videoStreamIndex.setAttribute("Chunks", Integer.toString(videoFragmentsDurations.length)); + videoStreamIndex.setAttribute("Url", "video/{bitrate}/{start time}"); + videoStreamIndex.setAttribute("QualityLevels", Integer.toString(videoQualities.size())); + smoothStreamingMedia.appendChild(videoStreamIndex); + + for (int i = 0; i < videoQualities.size(); i++) { + VideoQuality vq = videoQualities.get(i); + Element qualityLevel = document.createElement("QualityLevel"); + qualityLevel.setAttribute("Index", Integer.toString(i)); + qualityLevel.setAttribute("Bitrate", Long.toString(vq.bitrate)); + qualityLevel.setAttribute("FourCC", vq.fourCC); + qualityLevel.setAttribute("MaxWidth", Long.toString(vq.width)); + qualityLevel.setAttribute("MaxHeight", Long.toString(vq.height)); + qualityLevel.setAttribute("CodecPrivateData", vq.codecPrivateData); + qualityLevel.setAttribute("NALUnitLengthField", Integer.toString(vq.nalLength)); + videoStreamIndex.appendChild(qualityLevel); + } + + for (int i = 0; i < videoFragmentsDurations.length; i++) { + Element c = document.createElement("c"); + c.setAttribute("n", Integer.toString(i)); + c.setAttribute("d", Long.toString(videoFragmentsDurations[i])); + videoStreamIndex.appendChild(c); + } + + if (audioFragmentsDurations != null) { + Element audioStreamIndex = document.createElement("StreamIndex"); + audioStreamIndex.setAttribute("Type", "audio"); + audioStreamIndex.setAttribute("TimeScale", Long.toString(audioTimescale)); // silverlight ignores the timescale attr + audioStreamIndex.setAttribute("Chunks", Integer.toString(audioFragmentsDurations.length)); + audioStreamIndex.setAttribute("Url", "audio/{bitrate}/{start time}"); + audioStreamIndex.setAttribute("QualityLevels", Integer.toString(audioQualities.size())); + smoothStreamingMedia.appendChild(audioStreamIndex); + + for (int i = 0; i < audioQualities.size(); i++) { + AudioQuality aq = audioQualities.get(i); + Element qualityLevel = document.createElement("QualityLevel"); + qualityLevel.setAttribute("Index", Integer.toString(i)); + qualityLevel.setAttribute("FourCC", aq.fourCC); + qualityLevel.setAttribute("Bitrate", Long.toString(aq.bitrate)); + qualityLevel.setAttribute("AudioTag", Integer.toString(aq.audioTag)); + qualityLevel.setAttribute("SamplingRate", Long.toString(aq.samplingRate)); + qualityLevel.setAttribute("Channels", Integer.toString(aq.channels)); + qualityLevel.setAttribute("BitsPerSample", Integer.toString(aq.bitPerSample)); + qualityLevel.setAttribute("PacketSize", Integer.toString(aq.packetSize)); + qualityLevel.setAttribute("CodecPrivateData", aq.codecPrivateData); + audioStreamIndex.appendChild(qualityLevel); + } + for (int i = 0; i < audioFragmentsDurations.length; i++) { + Element c = document.createElement("c"); + c.setAttribute("n", Integer.toString(i)); + c.setAttribute("d", Long.toString(audioFragmentsDurations[i])); + audioStreamIndex.appendChild(c); + } + } + + document.setXmlStandalone(true); + Source source = new DOMSource(document); + StringWriter stringWriter = new StringWriter(); + Result result = new StreamResult(stringWriter); + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer; + try { + transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(source, result); + } catch (TransformerConfigurationException e) { + throw new IOException(e); + } catch (TransformerException e) { + throw new IOException(e); + } + return stringWriter.getBuffer().toString(); + + + } + + private AudioQuality getAudioQuality(Track track, AudioSampleEntry ase) { + if (getFormat(ase).equals("mp4a")) { + return getAacAudioQuality(track, ase); + } else if (getFormat(ase).equals("ec-3")) { + return getEc3AudioQuality(track, ase); + } else if (getFormat(ase).startsWith("dts")) { + return getDtsAudioQuality(track, ase); + } else { + throw new InternalError("I don't know what to do with audio of type " + getFormat(ase)); + } + + } + + private AudioQuality getAacAudioQuality(Track track, AudioSampleEntry ase) { + AudioQuality l = new AudioQuality(); + final ESDescriptorBox esDescriptorBox = ase.getBoxes(ESDescriptorBox.class).get(0); + final AudioSpecificConfig audioSpecificConfig = esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor().getAudioSpecificInfo(); + if (audioSpecificConfig.getSbrPresentFlag() == 1) { + l.fourCC = "AACH"; + } else if (audioSpecificConfig.getPsPresentFlag() == 1) { + l.fourCC = "AACP"; //I'm not sure if that's what MS considers as AAC+ - because actually AAC+ and AAC-HE should be the same... + } else { + l.fourCC = "AACL"; + } + l.bitrate = getBitrate(track); + l.audioTag = 255; + l.samplingRate = ase.getSampleRate(); + l.channels = ase.getChannelCount(); + l.bitPerSample = ase.getSampleSize(); + l.packetSize = 4; + l.codecPrivateData = getAudioCodecPrivateData(audioSpecificConfig); + //Index="0" Bitrate="103000" AudioTag="255" SamplingRate="44100" Channels="2" BitsPerSample="16" packetSize="4" CodecPrivateData="" + return l; + } + + private AudioQuality getEc3AudioQuality(Track track, AudioSampleEntry ase) { + final EC3SpecificBox ec3SpecificBox = ase.getBoxes(EC3SpecificBox.class).get(0); + if (ec3SpecificBox == null) { + throw new RuntimeException("EC-3 track misses EC3SpecificBox!"); + } + + short nfchans = 0; //full bandwidth channels + short lfechans = 0; + byte dWChannelMaskFirstByte = 0; + byte dWChannelMaskSecondByte = 0; + for (EC3SpecificBox.Entry entry : ec3SpecificBox.getEntries()) { + /* + Table 4.3: Audio coding mode + acmod Audio coding mode Nfchans Channel array ordering + 000 1 + 1 2 Ch1, Ch2 + 001 1/0 1 C + 010 2/0 2 L, R + 011 3/0 3 L, C, R + 100 2/1 3 L, R, S + 101 3/1 4 L, C, R, S + 110 2/2 4 L, R, SL, SR + 111 3/2 5 L, C, R, SL, SR + + Table F.2: Chan_loc field bit assignments + Bit Location + 0 Lc/Rc pair + 1 Lrs/Rrs pair + 2 Cs + 3 Ts + 4 Lsd/Rsd pair + 5 Lw/Rw pair + 6 Lvh/Rvh pair + 7 Cvh + 8 LFE2 + */ + switch (entry.acmod) { + case 0: //1+1; Ch1, Ch2 + nfchans += 2; + throw new RuntimeException("Smooth Streaming doesn't support DDP 1+1 mode"); + case 1: //1/0; C + nfchans += 1; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0x20; + } + break; + case 2: //2/0; L, R + nfchans += 2; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xC0; + } + break; + case 3: //3/0; L, C, R + nfchans += 3; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xE0; + } + break; + case 4: //2/1; L, R, S + nfchans += 3; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xC0; + dWChannelMaskSecondByte |= 0x80; + } + break; + case 5: //3/1; L, C, R, S + nfchans += 4; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xE0; + dWChannelMaskSecondByte |= 0x80; + } + break; + case 6: //2/2; L, R, SL, SR + nfchans += 4; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xCC; + } + break; + case 7: //3/2; L, C, R, SL, SR + nfchans += 5; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xEC; + } + break; + } + if (entry.lfeon == 1) { + lfechans ++; + dWChannelMaskFirstByte |= 0x10; + } + } + + final ByteBuffer waveformatex = ByteBuffer.allocate(22); + waveformatex.put(new byte[]{0x00, 0x06}); //1536 wSamplesPerBlock - little endian + waveformatex.put(dWChannelMaskFirstByte); + waveformatex.put(dWChannelMaskSecondByte); + waveformatex.put(new byte[]{0x00, 0x00}); //pad dwChannelMask to 32bit + waveformatex.put(new byte[]{(byte)0xAF, (byte)0x87, (byte)0xFB, (byte)0xA7, 0x02, 0x2D, (byte)0xFB, 0x42, (byte)0xA4, (byte)0xD4, 0x05, (byte)0xCD, (byte)0x93, (byte)0x84, 0x3B, (byte)0xDD}); //SubFormat - Dolby Digital Plus GUID + + final ByteBuffer dec3Content = ByteBuffer.allocate((int) ec3SpecificBox.getContentSize()); + ec3SpecificBox.getContent(dec3Content); + + AudioQuality l = new AudioQuality(); + l.fourCC = "EC-3"; + l.bitrate = getBitrate(track); + l.audioTag = 65534; + l.samplingRate = ase.getSampleRate(); + l.channels = nfchans + lfechans; + l.bitPerSample = 16; + l.packetSize = track.getSamples().get(0).limit(); //assuming all are same size + l.codecPrivateData = Hex.encodeHex(waveformatex.array()) + Hex.encodeHex(dec3Content.array()); //append EC3SpecificBox (big endian) at the end of waveformatex + return l; + } + + private AudioQuality getDtsAudioQuality(Track track, AudioSampleEntry ase) { + final DTSSpecificBox dtsSpecificBox = ase.getBoxes(DTSSpecificBox.class).get(0); + if (dtsSpecificBox == null) { + throw new RuntimeException("DTS track misses DTSSpecificBox!"); + } + + final ByteBuffer waveformatex = ByteBuffer.allocate(22); + final int frameDuration = dtsSpecificBox.getFrameDuration(); + short samplesPerBlock = 0; + switch (frameDuration) { + case 0: + samplesPerBlock = 512; + break; + case 1: + samplesPerBlock = 1024; + break; + case 2: + samplesPerBlock = 2048; + break; + case 3: + samplesPerBlock = 4096; + break; + } + waveformatex.put((byte) (samplesPerBlock & 0xff)); + waveformatex.put((byte) (samplesPerBlock >>> 8)); + final int dwChannelMask = getNumChannelsAndMask(dtsSpecificBox)[1]; + waveformatex.put((byte) (dwChannelMask & 0xff)); + waveformatex.put((byte) (dwChannelMask >>> 8)); + waveformatex.put((byte) (dwChannelMask >>> 16)); + waveformatex.put((byte) (dwChannelMask >>> 24)); + waveformatex.put(new byte[]{(byte)0xAE, (byte)0xE4, (byte)0xBF, (byte)0x5E, (byte)0x61, (byte)0x5E, (byte)0x41, (byte)0x87, (byte)0x92, (byte)0xFC, (byte)0xA4, (byte)0x81, (byte)0x26, (byte)0x99, (byte)0x02, (byte)0x11}); //DTS-HD GUID + + final ByteBuffer dtsCodecPrivateData = ByteBuffer.allocate(8); + dtsCodecPrivateData.put((byte) dtsSpecificBox.getStreamConstruction()); + + final int channelLayout = dtsSpecificBox.getChannelLayout(); + dtsCodecPrivateData.put((byte) (channelLayout & 0xff)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 8)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 16)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 24)); + + byte dtsFlags = (byte) (dtsSpecificBox.getMultiAssetFlag() << 1); + dtsFlags |= dtsSpecificBox.getLBRDurationMod(); + dtsCodecPrivateData.put(dtsFlags); + dtsCodecPrivateData.put(new byte[]{0x00, 0x00}); //reserved + + AudioQuality l = new AudioQuality(); + l.fourCC = getFormat(ase); + l.bitrate = dtsSpecificBox.getAvgBitRate(); + l.audioTag = 65534; + l.samplingRate = dtsSpecificBox.getDTSSamplingFrequency(); + l.channels = getNumChannelsAndMask(dtsSpecificBox)[0]; + l.bitPerSample = 16; + l.packetSize = track.getSamples().get(0).limit(); //assuming all are same size + l.codecPrivateData = Hex.encodeHex(waveformatex.array()) + Hex.encodeHex(dtsCodecPrivateData.array()); + return l; + + } + + /* dwChannelMask + L SPEAKER_FRONT_LEFT 0x00000001 + R SPEAKER_FRONT_RIGHT 0x00000002 + C SPEAKER_FRONT_CENTER 0x00000004 + LFE1 SPEAKER_LOW_FREQUENCY 0x00000008 + Ls or Lsr* SPEAKER_BACK_LEFT 0x00000010 + Rs or Rsr* SPEAKER_BACK_RIGHT 0x00000020 + Lc SPEAKER_FRONT_LEFT_OF_CENTER 0x00000040 + Rc SPEAKER_FRONT_RIGHT_OF_CENTER 0x00000080 + Cs SPEAKER_BACK_CENTER 0x00000100 + Lss SPEAKER_SIDE_LEFT 0x00000200 + Rss SPEAKER_SIDE_RIGHT 0x00000400 + Oh SPEAKER_TOP_CENTER 0x00000800 + Lh SPEAKER_TOP_FRONT_LEFT 0x00001000 + Ch SPEAKER_TOP_FRONT_CENTER 0x00002000 + Rh SPEAKER_TOP_FRONT_RIGHT 0x00004000 + Lhr SPEAKER_TOP_BACK_LEFT 0x00008000 + Chf SPEAKER_TOP_BACK_CENTER 0x00010000 + Rhr SPEAKER_TOP_BACK_RIGHT 0x00020000 + SPEAKER_RESERVED 0x80000000 + + * if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + */ + private int[] getNumChannelsAndMask(DTSSpecificBox dtsSpecificBox) { + final int channelLayout = dtsSpecificBox.getChannelLayout(); + int numChannels = 0; + int dwChannelMask = 0; + if ((channelLayout & 0x0001) == 0x0001) { + //0001h Center in front of listener 1 + numChannels += 1; + dwChannelMask |= 0x00000004; //SPEAKER_FRONT_CENTER + } + if ((channelLayout & 0x0002) == 0x0002) { + //0002h Left/Right in front 2 + numChannels += 2; + dwChannelMask |= 0x00000001; //SPEAKER_FRONT_LEFT + dwChannelMask |= 0x00000002; //SPEAKER_FRONT_RIGHT + } + if ((channelLayout & 0x0004) == 0x0004) { + //0004h Left/Right surround on side in rear 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0008) == 0x0008) { + //0008h Low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x0010) == 0x0010) { + //0010h Center surround in rear 1 + numChannels += 1; + dwChannelMask |= 0x00000100; //SPEAKER_BACK_CENTER + } + if ((channelLayout & 0x0020) == 0x0020) { + //0020h Left/Right height in front 2 + numChannels += 2; + dwChannelMask |= 0x00001000; //SPEAKER_TOP_FRONT_LEFT + dwChannelMask |= 0x00004000; //SPEAKER_TOP_FRONT_RIGHT + } + if ((channelLayout & 0x0040) == 0x0040) { + //0040h Left/Right surround in rear 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0080) == 0x0080) { + //0080h Center Height in front 1 + numChannels += 1; + dwChannelMask |= 0x00002000; //SPEAKER_TOP_FRONT_CENTER + } + if ((channelLayout & 0x0100) == 0x0100) { + //0100h Over the listener’s head 1 + numChannels += 1; + dwChannelMask |= 0x00000800; //SPEAKER_TOP_CENTER + } + if ((channelLayout & 0x0200) == 0x0200) { + //0200h Between left/right and center in front 2 + numChannels += 2; + dwChannelMask |= 0x00000040; //SPEAKER_FRONT_LEFT_OF_CENTER + dwChannelMask |= 0x00000080; //SPEAKER_FRONT_RIGHT_OF_CENTER + } + if ((channelLayout & 0x0400) == 0x0400) { + //0400h Left/Right on side in front 2 + numChannels += 2; + dwChannelMask |= 0x00000200; //SPEAKER_SIDE_LEFT + dwChannelMask |= 0x00000400; //SPEAKER_SIDE_RIGHT + } + if ((channelLayout & 0x0800) == 0x0800) { + //0800h Left/Right surround on side 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x1000) == 0x1000) { + //1000h Second low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x2000) == 0x2000) { + //2000h Left/Right height on side 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x4000) == 0x4000) { + //4000h Center height in rear 1 + numChannels += 1; + dwChannelMask |= 0x00010000; //SPEAKER_TOP_BACK_CENTER + } + if ((channelLayout & 0x8000) == 0x8000) { + //8000h Left/Right height in rear 2 + numChannels += 2; + dwChannelMask |= 0x00008000; //SPEAKER_TOP_BACK_LEFT + dwChannelMask |= 0x00020000; //SPEAKER_TOP_BACK_RIGHT + } + if ((channelLayout & 0x10000) == 0x10000) { + //10000h Center below in front + numChannels += 1; + } + if ((channelLayout & 0x20000) == 0x20000) { + //20000h Left/Right below in front + numChannels += 2; + } + return new int[]{numChannels, dwChannelMask}; + } + + private String getAudioCodecPrivateData(AudioSpecificConfig audioSpecificConfig) { + byte[] configByteArray = audioSpecificConfig.getConfigBytes(); + return Hex.encodeHex(configByteArray); + } + + private VideoQuality getVideoQuality(Track track, VisualSampleEntry vse) { + VideoQuality l; + if ("avc1".equals(getFormat(vse))) { + AvcConfigurationBox avcConfigurationBox = vse.getBoxes(AvcConfigurationBox.class).get(0); + l = new VideoQuality(); + l.bitrate = getBitrate(track); + l.codecPrivateData = Hex.encodeHex(getAvcCodecPrivateData(avcConfigurationBox)); + l.fourCC = "AVC1"; + l.width = vse.getWidth(); + l.height = vse.getHeight(); + l.nalLength = avcConfigurationBox.getLengthSizeMinusOne() + 1; + } else { + throw new InternalError("I don't know how to handle video of type " + getFormat(vse)); + } + return l; + } + + private byte[] getAvcCodecPrivateData(AvcConfigurationBox avcConfigurationBox) { + List<byte[]> sps = avcConfigurationBox.getSequenceParameterSets(); + List<byte[]> pps = avcConfigurationBox.getPictureParameterSets(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + baos.write(new byte[]{0, 0, 0, 1}); + + for (byte[] sp : sps) { + baos.write(sp); + } + baos.write(new byte[]{0, 0, 0, 1}); + for (byte[] pp : pps) { + baos.write(pp); + } + } catch (IOException ex) { + throw new RuntimeException("ByteArrayOutputStream do not throw IOException ?!?!?"); + } + return baos.toByteArray(); + } + + private class DependentSubstreamMask { + private byte dWChannelMaskFirstByte; + private byte dWChannelMaskSecondByte; + private EC3SpecificBox.Entry entry; + + public DependentSubstreamMask(byte dWChannelMaskFirstByte, byte dWChannelMaskSecondByte, EC3SpecificBox.Entry entry) { + this.dWChannelMaskFirstByte = dWChannelMaskFirstByte; + this.dWChannelMaskSecondByte = dWChannelMaskSecondByte; + this.entry = entry; + } + + public byte getdWChannelMaskFirstByte() { + return dWChannelMaskFirstByte; + } + + public byte getdWChannelMaskSecondByte() { + return dWChannelMaskSecondByte; + } + + public DependentSubstreamMask process() { + switch (entry.chan_loc) { + case 0: + dWChannelMaskFirstByte |= 0x3; + break; + case 1: + dWChannelMaskFirstByte |= 0xC; + break; + case 2: + dWChannelMaskSecondByte |= 0x80; + break; + case 3: + dWChannelMaskSecondByte |= 0x8; + break; + case 6: + dWChannelMaskSecondByte |= 0x5; + break; + case 7: + dWChannelMaskSecondByte |= 0x2; + break; + } + return this; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatPackageWriterImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatPackageWriterImpl.java.svn-base new file mode 100644 index 0000000..3e3847c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/FlatPackageWriterImpl.java.svn-base @@ -0,0 +1,197 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.SoundMediaHeaderBox; +import com.coremedia.iso.boxes.VideoMediaHeaderBox; +import com.coremedia.iso.boxes.fragment.MovieFragmentBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.*; +import com.googlecode.mp4parser.authoring.tracks.ChangeTimeScaleTrack; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.Iterator; +import java.util.logging.Logger; + +public class FlatPackageWriterImpl implements PackageWriter { + private static Logger LOG = Logger.getLogger(FlatPackageWriterImpl.class.getName()); + long timeScale = 10000000; + + private File outputDirectory; + private boolean debugOutput; + private FragmentedMp4Builder ismvBuilder; + ManifestWriter manifestWriter; + + public FlatPackageWriterImpl() { + ismvBuilder = new FragmentedMp4Builder(); + FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl(); + ismvBuilder.setIntersectionFinder(intersectionFinder); + manifestWriter = new FlatManifestWriterImpl(intersectionFinder); + } + + /** + * Creates a factory for a smooth streaming package. A smooth streaming package is + * a collection of files that can be served by a webserver as a smooth streaming + * stream. + * @param minFragmentDuration the smallest allowable duration of a fragment (0 == no restriction). + */ + public FlatPackageWriterImpl(int minFragmentDuration) { + ismvBuilder = new FragmentedMp4Builder(); + FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl(minFragmentDuration); + ismvBuilder.setIntersectionFinder(intersectionFinder); + manifestWriter = new FlatManifestWriterImpl(intersectionFinder); + } + + public void setOutputDirectory(File outputDirectory) { + assert outputDirectory.isDirectory(); + this.outputDirectory = outputDirectory; + + } + + public void setDebugOutput(boolean debugOutput) { + this.debugOutput = debugOutput; + } + + public void setIsmvBuilder(FragmentedMp4Builder ismvBuilder) { + this.ismvBuilder = ismvBuilder; + this.manifestWriter = new FlatManifestWriterImpl(ismvBuilder.getFragmentIntersectionFinder()); + } + + public void setManifestWriter(ManifestWriter manifestWriter) { + this.manifestWriter = manifestWriter; + } + + /** + * Writes the movie given as <code>qualities</code> flattened into the + * <code>outputDirectory</code>. + * + * @param source the source movie with all qualities + * @throws IOException + */ + public void write(Movie source) throws IOException { + + if (debugOutput) { + outputDirectory.mkdirs(); + DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder(); + IsoFile muxed = defaultMp4Builder.build(source); + File muxedFile = new File(outputDirectory, "debug_1_muxed.mp4"); + FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile); + muxed.getBox(muxedFileOutputStream.getChannel()); + muxedFileOutputStream.close(); + } + Movie cleanedSource = removeUnknownTracks(source); + Movie movieWithAdjustedTimescale = correctTimescale(cleanedSource); + + if (debugOutput) { + DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder(); + IsoFile muxed = defaultMp4Builder.build(movieWithAdjustedTimescale); + File muxedFile = new File(outputDirectory, "debug_2_timescale.mp4"); + FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile); + muxed.getBox(muxedFileOutputStream.getChannel()); + muxedFileOutputStream.close(); + } + IsoFile isoFile = ismvBuilder.build(movieWithAdjustedTimescale); + if (debugOutput) { + File allQualities = new File(outputDirectory, "debug_3_fragmented.mp4"); + FileOutputStream allQualis = new FileOutputStream(allQualities); + isoFile.getBox(allQualis.getChannel()); + allQualis.close(); + } + + + for (Track track : movieWithAdjustedTimescale.getTracks()) { + String bitrate = Long.toString(manifestWriter.getBitrate(track)); + long trackId = track.getTrackMetaData().getTrackId(); + Iterator<Box> boxIt = isoFile.getBoxes().iterator(); + File mediaOutDir; + if (track.getMediaHeaderBox() instanceof SoundMediaHeaderBox) { + mediaOutDir = new File(outputDirectory, "audio"); + + } else if (track.getMediaHeaderBox() instanceof VideoMediaHeaderBox) { + mediaOutDir = new File(outputDirectory, "video"); + } else { + System.err.println("Skipping Track with handler " + track.getHandler() + " and " + track.getMediaHeaderBox().getClass().getSimpleName()); + continue; + } + File bitRateOutputDir = new File(mediaOutDir, bitrate); + bitRateOutputDir.mkdirs(); + LOG.finer("Created : " + bitRateOutputDir.getCanonicalPath()); + + long[] fragmentTimes = manifestWriter.calculateFragmentDurations(track, movieWithAdjustedTimescale); + long startTime = 0; + int currentFragment = 0; + while (boxIt.hasNext()) { + Box b = boxIt.next(); + if (b instanceof MovieFragmentBox) { + assert ((MovieFragmentBox) b).getTrackCount() == 1; + if (((MovieFragmentBox) b).getTrackNumbers()[0] == trackId) { + FileOutputStream fos = new FileOutputStream(new File(bitRateOutputDir, Long.toString(startTime))); + startTime += fragmentTimes[currentFragment++]; + FileChannel fc = fos.getChannel(); + Box mdat = boxIt.next(); + assert mdat.getType().equals("mdat"); + b.getBox(fc); // moof + mdat.getBox(fc); // mdat + fc.truncate(fc.position()); + fc.close(); + } + } + + } + } + FileWriter fw = new FileWriter(new File(outputDirectory, "Manifest")); + fw.write(manifestWriter.getManifest(movieWithAdjustedTimescale)); + fw.close(); + + } + + private Movie removeUnknownTracks(Movie source) { + Movie nuMovie = new Movie(); + for (Track track : source.getTracks()) { + if ("vide".equals(track.getHandler()) || "soun".equals(track.getHandler())) { + nuMovie.addTrack(track); + } else { + LOG.fine("Removed track " + track); + } + } + return nuMovie; + } + + + /** + * Returns a new <code>Movie</code> in that all tracks have the timescale 10000000. CTS & DTS are modified + * in a way that even with more than one framerate the fragments exactly begin at the same time. + * + * @param movie + * @return a movie with timescales suitable for smooth streaming manifests + */ + public Movie correctTimescale(Movie movie) { + Movie nuMovie = new Movie(); + for (Track track : movie.getTracks()) { + nuMovie.addTrack(new ChangeTimeScaleTrack(track, timeScale, ismvBuilder.getFragmentIntersectionFinder().sampleNumbers(track, movie))); + } + return nuMovie; + + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/ManifestWriter.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/ManifestWriter.java.svn-base new file mode 100644 index 0000000..2b2ba7d --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/ManifestWriter.java.svn-base @@ -0,0 +1,31 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + + +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; + +public interface ManifestWriter { + String getManifest(Movie inputs) throws IOException; + + long getBitrate(Track track); + + long[] calculateFragmentDurations(Track track, Movie movie); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/PackageWriter.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/PackageWriter.java.svn-base new file mode 100644 index 0000000..0d97fc5 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/PackageWriter.java.svn-base @@ -0,0 +1,27 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.googlecode.mp4parser.authoring.Movie; + +import java.io.IOException; + +/** + * Writes the whole package. + */ +public interface PackageWriter { + public void write(Movie qualities) throws IOException; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/VideoQuality.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/VideoQuality.java.svn-base new file mode 100644 index 0000000..4a70e47 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/.svn/text-base/VideoQuality.java.svn-base @@ -0,0 +1,25 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +class VideoQuality { + long bitrate; + String fourCC; + int width; + int height; + String codecPrivateData; + int nalLength; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AbstractManifestWriter.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AbstractManifestWriter.java new file mode 100644 index 0000000..6ee4ffa --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AbstractManifestWriter.java @@ -0,0 +1,126 @@ +package com.googlecode.mp4parser.authoring.adaptivestreaming;
+
+import com.coremedia.iso.boxes.OriginalFormatBox;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.coremedia.iso.boxes.sampleentry.SampleEntry;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static com.googlecode.mp4parser.util.CastUtils.l2i;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: mstattma
+ * Date: 17.08.12
+ * Time: 02:51
+ * To change this template use File | Settings | File Templates.
+ */
+public abstract class AbstractManifestWriter implements ManifestWriter {
+ private static final Logger LOG = Logger.getLogger(AbstractManifestWriter.class.getName());
+
+ private FragmentIntersectionFinder intersectionFinder;
+ protected long[] audioFragmentsDurations;
+ protected long[] videoFragmentsDurations;
+
+ protected AbstractManifestWriter(FragmentIntersectionFinder intersectionFinder) {
+ this.intersectionFinder = intersectionFinder;
+ }
+
+ /**
+ * Calculates the length of each fragment in the given <code>track</code> (as part of <code>movie</code>).
+ *
+ * @param track target of calculation
+ * @param movie the <code>track</code> must be part of this <code>movie</code>
+ * @return the duration of each fragment in track timescale
+ */
+ public long[] calculateFragmentDurations(Track track, Movie movie) {
+ long[] startSamples = intersectionFinder.sampleNumbers(track, movie);
+ long[] durations = new long[startSamples.length];
+ int currentFragment = 0;
+ int currentSample = 1; // sync samples start with 1 !
+
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ for (int max = currentSample + l2i(entry.getCount()); currentSample < max; currentSample++) {
+ // in this loop we go through the entry.getCount() samples starting from current sample.
+ // the next entry.getCount() samples have the same decoding time.
+ if (currentFragment != startSamples.length - 1 && currentSample == startSamples[currentFragment + 1]) {
+ // we are not in the last fragment && the current sample is the start sample of the next fragment
+ currentFragment++;
+ }
+ durations[currentFragment] += entry.getDelta();
+
+
+ }
+ }
+ return durations;
+
+ }
+
+ public long getBitrate(Track track) {
+ long bitrate = 0;
+ for (ByteBuffer sample : track.getSamples()) {
+ bitrate += sample.limit();
+ }
+ bitrate *= 8; // from bytes to bits
+ bitrate /= ((double) getDuration(track)) / track.getTrackMetaData().getTimescale(); // per second
+ return bitrate;
+ }
+
+ protected static long getDuration(Track track) {
+ long duration = 0;
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ duration += entry.getCount() * entry.getDelta();
+ }
+ return duration;
+ }
+
+ protected long[] checkFragmentsAlign(long[] referenceTimes, long[] checkTimes) throws IOException {
+
+ if (referenceTimes == null || referenceTimes.length == 0) {
+ return checkTimes;
+ }
+ long[] referenceTimesMinusLast = new long[referenceTimes.length - 1];
+ System.arraycopy(referenceTimes, 0, referenceTimesMinusLast, 0, referenceTimes.length - 1);
+ long[] checkTimesMinusLast = new long[checkTimes.length - 1];
+ System.arraycopy(checkTimes, 0, checkTimesMinusLast, 0, checkTimes.length - 1);
+
+ if (!Arrays.equals(checkTimesMinusLast, referenceTimesMinusLast)) {
+ String log = "";
+ log += (referenceTimes.length);
+ log += ("Reference : [");
+ for (long l : referenceTimes) {
+ log += (String.format("%10d,", l));
+ }
+ log += ("]");
+ LOG.warning(log);
+ log = "";
+
+ log += (checkTimes.length);
+ log += ("Current : [");
+ for (long l : checkTimes) {
+ log += (String.format("%10d,", l));
+ }
+ log += ("]");
+ LOG.warning(log);
+ throw new IOException("Track does not have the same fragment borders as its predecessor.");
+
+ } else {
+ return checkTimes;
+ }
+ }
+
+ protected String getFormat(SampleEntry se) {
+ String type = se.getType();
+ if (type.equals("encv") || type.equals("enca") || type.equals("encv")) {
+ OriginalFormatBox frma = se.getBoxes(OriginalFormatBox.class, true).get(0);
+ type = frma.getDataFormat();
+ }
+ return type;
+ }
+}
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AudioQuality.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AudioQuality.java new file mode 100644 index 0000000..39e115f --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/AudioQuality.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + + +public class AudioQuality { + String fourCC; + long bitrate; + int audioTag; + long samplingRate; + int channels; + int bitPerSample; + int packetSize; + String language; + String codecPrivateData; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatManifestWriterImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatManifestWriterImpl.java new file mode 100644 index 0000000..5cc9be9 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatManifestWriterImpl.java @@ -0,0 +1,643 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.coremedia.iso.Hex; +import com.coremedia.iso.boxes.SampleDescriptionBox; +import com.coremedia.iso.boxes.SoundMediaHeaderBox; +import com.coremedia.iso.boxes.VideoMediaHeaderBox; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.Version; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder; +import com.googlecode.mp4parser.boxes.DTSSpecificBox; +import com.googlecode.mp4parser.boxes.EC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +public class FlatManifestWriterImpl extends AbstractManifestWriter { + private static final Logger LOG = Logger.getLogger(FlatManifestWriterImpl.class.getName()); + + protected FlatManifestWriterImpl(FragmentIntersectionFinder intersectionFinder) { + super(intersectionFinder); + } + + /** + * Overwrite this method in subclasses to add your specialities. + * + * @param manifest the original manifest + * @return your customized version of the manifest + */ + protected Document customizeManifest(Document manifest) { + return manifest; + } + + public String getManifest(Movie movie) throws IOException { + + LinkedList<VideoQuality> videoQualities = new LinkedList<VideoQuality>(); + long videoTimescale = -1; + + LinkedList<AudioQuality> audioQualities = new LinkedList<AudioQuality>(); + long audioTimescale = -1; + + for (Track track : movie.getTracks()) { + if (track.getMediaHeaderBox() instanceof VideoMediaHeaderBox) { + videoFragmentsDurations = checkFragmentsAlign(videoFragmentsDurations, calculateFragmentDurations(track, movie)); + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + videoQualities.add(getVideoQuality(track, (VisualSampleEntry) stsd.getSampleEntry())); + if (videoTimescale == -1) { + videoTimescale = track.getTrackMetaData().getTimescale(); + } else { + assert videoTimescale == track.getTrackMetaData().getTimescale(); + } + } + if (track.getMediaHeaderBox() instanceof SoundMediaHeaderBox) { + audioFragmentsDurations = checkFragmentsAlign(audioFragmentsDurations, calculateFragmentDurations(track, movie)); + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + audioQualities.add(getAudioQuality(track, (AudioSampleEntry) stsd.getSampleEntry())); + if (audioTimescale == -1) { + audioTimescale = track.getTrackMetaData().getTimescale(); + } else { + assert audioTimescale == track.getTrackMetaData().getTimescale(); + } + + } + } + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder; + try { + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IOException(e); + } + Document document = documentBuilder.newDocument(); + + + Element smoothStreamingMedia = document.createElement("SmoothStreamingMedia"); + document.appendChild(smoothStreamingMedia); + smoothStreamingMedia.setAttribute("MajorVersion", "2"); + smoothStreamingMedia.setAttribute("MinorVersion", "1"); +// silverlight ignores the timescale attr smoothStreamingMedia.addAttribute(new Attribute("TimeScale", Long.toString(movieTimeScale))); + smoothStreamingMedia.setAttribute("Duration", "0"); + + smoothStreamingMedia.appendChild(document.createComment(Version.VERSION)); + Element videoStreamIndex = document.createElement("StreamIndex"); + videoStreamIndex.setAttribute("Type", "video"); + videoStreamIndex.setAttribute("TimeScale", Long.toString(videoTimescale)); // silverlight ignores the timescale attr + videoStreamIndex.setAttribute("Chunks", Integer.toString(videoFragmentsDurations.length)); + videoStreamIndex.setAttribute("Url", "video/{bitrate}/{start time}"); + videoStreamIndex.setAttribute("QualityLevels", Integer.toString(videoQualities.size())); + smoothStreamingMedia.appendChild(videoStreamIndex); + + for (int i = 0; i < videoQualities.size(); i++) { + VideoQuality vq = videoQualities.get(i); + Element qualityLevel = document.createElement("QualityLevel"); + qualityLevel.setAttribute("Index", Integer.toString(i)); + qualityLevel.setAttribute("Bitrate", Long.toString(vq.bitrate)); + qualityLevel.setAttribute("FourCC", vq.fourCC); + qualityLevel.setAttribute("MaxWidth", Long.toString(vq.width)); + qualityLevel.setAttribute("MaxHeight", Long.toString(vq.height)); + qualityLevel.setAttribute("CodecPrivateData", vq.codecPrivateData); + qualityLevel.setAttribute("NALUnitLengthField", Integer.toString(vq.nalLength)); + videoStreamIndex.appendChild(qualityLevel); + } + + for (int i = 0; i < videoFragmentsDurations.length; i++) { + Element c = document.createElement("c"); + c.setAttribute("n", Integer.toString(i)); + c.setAttribute("d", Long.toString(videoFragmentsDurations[i])); + videoStreamIndex.appendChild(c); + } + + if (audioFragmentsDurations != null) { + Element audioStreamIndex = document.createElement("StreamIndex"); + audioStreamIndex.setAttribute("Type", "audio"); + audioStreamIndex.setAttribute("TimeScale", Long.toString(audioTimescale)); // silverlight ignores the timescale attr + audioStreamIndex.setAttribute("Chunks", Integer.toString(audioFragmentsDurations.length)); + audioStreamIndex.setAttribute("Url", "audio/{bitrate}/{start time}"); + audioStreamIndex.setAttribute("QualityLevels", Integer.toString(audioQualities.size())); + smoothStreamingMedia.appendChild(audioStreamIndex); + + for (int i = 0; i < audioQualities.size(); i++) { + AudioQuality aq = audioQualities.get(i); + Element qualityLevel = document.createElement("QualityLevel"); + qualityLevel.setAttribute("Index", Integer.toString(i)); + qualityLevel.setAttribute("FourCC", aq.fourCC); + qualityLevel.setAttribute("Bitrate", Long.toString(aq.bitrate)); + qualityLevel.setAttribute("AudioTag", Integer.toString(aq.audioTag)); + qualityLevel.setAttribute("SamplingRate", Long.toString(aq.samplingRate)); + qualityLevel.setAttribute("Channels", Integer.toString(aq.channels)); + qualityLevel.setAttribute("BitsPerSample", Integer.toString(aq.bitPerSample)); + qualityLevel.setAttribute("PacketSize", Integer.toString(aq.packetSize)); + qualityLevel.setAttribute("CodecPrivateData", aq.codecPrivateData); + audioStreamIndex.appendChild(qualityLevel); + } + for (int i = 0; i < audioFragmentsDurations.length; i++) { + Element c = document.createElement("c"); + c.setAttribute("n", Integer.toString(i)); + c.setAttribute("d", Long.toString(audioFragmentsDurations[i])); + audioStreamIndex.appendChild(c); + } + } + + document.setXmlStandalone(true); + Source source = new DOMSource(document); + StringWriter stringWriter = new StringWriter(); + Result result = new StreamResult(stringWriter); + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer; + try { + transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(source, result); + } catch (TransformerConfigurationException e) { + throw new IOException(e); + } catch (TransformerException e) { + throw new IOException(e); + } + return stringWriter.getBuffer().toString(); + + + } + + private AudioQuality getAudioQuality(Track track, AudioSampleEntry ase) { + if (getFormat(ase).equals("mp4a")) { + return getAacAudioQuality(track, ase); + } else if (getFormat(ase).equals("ec-3")) { + return getEc3AudioQuality(track, ase); + } else if (getFormat(ase).startsWith("dts")) { + return getDtsAudioQuality(track, ase); + } else { + throw new InternalError("I don't know what to do with audio of type " + getFormat(ase)); + } + + } + + private AudioQuality getAacAudioQuality(Track track, AudioSampleEntry ase) { + AudioQuality l = new AudioQuality(); + final ESDescriptorBox esDescriptorBox = ase.getBoxes(ESDescriptorBox.class).get(0); + final AudioSpecificConfig audioSpecificConfig = esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor().getAudioSpecificInfo(); + if (audioSpecificConfig.getSbrPresentFlag() == 1) { + l.fourCC = "AACH"; + } else if (audioSpecificConfig.getPsPresentFlag() == 1) { + l.fourCC = "AACP"; //I'm not sure if that's what MS considers as AAC+ - because actually AAC+ and AAC-HE should be the same... + } else { + l.fourCC = "AACL"; + } + l.bitrate = getBitrate(track); + l.audioTag = 255; + l.samplingRate = ase.getSampleRate(); + l.channels = ase.getChannelCount(); + l.bitPerSample = ase.getSampleSize(); + l.packetSize = 4; + l.codecPrivateData = getAudioCodecPrivateData(audioSpecificConfig); + //Index="0" Bitrate="103000" AudioTag="255" SamplingRate="44100" Channels="2" BitsPerSample="16" packetSize="4" CodecPrivateData="" + return l; + } + + private AudioQuality getEc3AudioQuality(Track track, AudioSampleEntry ase) { + final EC3SpecificBox ec3SpecificBox = ase.getBoxes(EC3SpecificBox.class).get(0); + if (ec3SpecificBox == null) { + throw new RuntimeException("EC-3 track misses EC3SpecificBox!"); + } + + short nfchans = 0; //full bandwidth channels + short lfechans = 0; + byte dWChannelMaskFirstByte = 0; + byte dWChannelMaskSecondByte = 0; + for (EC3SpecificBox.Entry entry : ec3SpecificBox.getEntries()) { + /* + Table 4.3: Audio coding mode + acmod Audio coding mode Nfchans Channel array ordering + 000 1 + 1 2 Ch1, Ch2 + 001 1/0 1 C + 010 2/0 2 L, R + 011 3/0 3 L, C, R + 100 2/1 3 L, R, S + 101 3/1 4 L, C, R, S + 110 2/2 4 L, R, SL, SR + 111 3/2 5 L, C, R, SL, SR + + Table F.2: Chan_loc field bit assignments + Bit Location + 0 Lc/Rc pair + 1 Lrs/Rrs pair + 2 Cs + 3 Ts + 4 Lsd/Rsd pair + 5 Lw/Rw pair + 6 Lvh/Rvh pair + 7 Cvh + 8 LFE2 + */ + switch (entry.acmod) { + case 0: //1+1; Ch1, Ch2 + nfchans += 2; + throw new RuntimeException("Smooth Streaming doesn't support DDP 1+1 mode"); + case 1: //1/0; C + nfchans += 1; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0x20; + } + break; + case 2: //2/0; L, R + nfchans += 2; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xC0; + } + break; + case 3: //3/0; L, C, R + nfchans += 3; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xE0; + } + break; + case 4: //2/1; L, R, S + nfchans += 3; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xC0; + dWChannelMaskSecondByte |= 0x80; + } + break; + case 5: //3/1; L, C, R, S + nfchans += 4; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xE0; + dWChannelMaskSecondByte |= 0x80; + } + break; + case 6: //2/2; L, R, SL, SR + nfchans += 4; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xCC; + } + break; + case 7: //3/2; L, C, R, SL, SR + nfchans += 5; + if (entry.num_dep_sub > 0) { + DependentSubstreamMask dependentSubstreamMask = new DependentSubstreamMask(dWChannelMaskFirstByte, dWChannelMaskSecondByte, entry).process(); + dWChannelMaskFirstByte |= dependentSubstreamMask.getdWChannelMaskFirstByte(); + dWChannelMaskSecondByte |= dependentSubstreamMask.getdWChannelMaskSecondByte(); + } else { + dWChannelMaskFirstByte |= 0xEC; + } + break; + } + if (entry.lfeon == 1) { + lfechans ++; + dWChannelMaskFirstByte |= 0x10; + } + } + + final ByteBuffer waveformatex = ByteBuffer.allocate(22); + waveformatex.put(new byte[]{0x00, 0x06}); //1536 wSamplesPerBlock - little endian + waveformatex.put(dWChannelMaskFirstByte); + waveformatex.put(dWChannelMaskSecondByte); + waveformatex.put(new byte[]{0x00, 0x00}); //pad dwChannelMask to 32bit + waveformatex.put(new byte[]{(byte)0xAF, (byte)0x87, (byte)0xFB, (byte)0xA7, 0x02, 0x2D, (byte)0xFB, 0x42, (byte)0xA4, (byte)0xD4, 0x05, (byte)0xCD, (byte)0x93, (byte)0x84, 0x3B, (byte)0xDD}); //SubFormat - Dolby Digital Plus GUID + + final ByteBuffer dec3Content = ByteBuffer.allocate((int) ec3SpecificBox.getContentSize()); + ec3SpecificBox.getContent(dec3Content); + + AudioQuality l = new AudioQuality(); + l.fourCC = "EC-3"; + l.bitrate = getBitrate(track); + l.audioTag = 65534; + l.samplingRate = ase.getSampleRate(); + l.channels = nfchans + lfechans; + l.bitPerSample = 16; + l.packetSize = track.getSamples().get(0).limit(); //assuming all are same size + l.codecPrivateData = Hex.encodeHex(waveformatex.array()) + Hex.encodeHex(dec3Content.array()); //append EC3SpecificBox (big endian) at the end of waveformatex + return l; + } + + private AudioQuality getDtsAudioQuality(Track track, AudioSampleEntry ase) { + final DTSSpecificBox dtsSpecificBox = ase.getBoxes(DTSSpecificBox.class).get(0); + if (dtsSpecificBox == null) { + throw new RuntimeException("DTS track misses DTSSpecificBox!"); + } + + final ByteBuffer waveformatex = ByteBuffer.allocate(22); + final int frameDuration = dtsSpecificBox.getFrameDuration(); + short samplesPerBlock = 0; + switch (frameDuration) { + case 0: + samplesPerBlock = 512; + break; + case 1: + samplesPerBlock = 1024; + break; + case 2: + samplesPerBlock = 2048; + break; + case 3: + samplesPerBlock = 4096; + break; + } + waveformatex.put((byte) (samplesPerBlock & 0xff)); + waveformatex.put((byte) (samplesPerBlock >>> 8)); + final int dwChannelMask = getNumChannelsAndMask(dtsSpecificBox)[1]; + waveformatex.put((byte) (dwChannelMask & 0xff)); + waveformatex.put((byte) (dwChannelMask >>> 8)); + waveformatex.put((byte) (dwChannelMask >>> 16)); + waveformatex.put((byte) (dwChannelMask >>> 24)); + waveformatex.put(new byte[]{(byte)0xAE, (byte)0xE4, (byte)0xBF, (byte)0x5E, (byte)0x61, (byte)0x5E, (byte)0x41, (byte)0x87, (byte)0x92, (byte)0xFC, (byte)0xA4, (byte)0x81, (byte)0x26, (byte)0x99, (byte)0x02, (byte)0x11}); //DTS-HD GUID + + final ByteBuffer dtsCodecPrivateData = ByteBuffer.allocate(8); + dtsCodecPrivateData.put((byte) dtsSpecificBox.getStreamConstruction()); + + final int channelLayout = dtsSpecificBox.getChannelLayout(); + dtsCodecPrivateData.put((byte) (channelLayout & 0xff)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 8)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 16)); + dtsCodecPrivateData.put((byte) (channelLayout >>> 24)); + + byte dtsFlags = (byte) (dtsSpecificBox.getMultiAssetFlag() << 1); + dtsFlags |= dtsSpecificBox.getLBRDurationMod(); + dtsCodecPrivateData.put(dtsFlags); + dtsCodecPrivateData.put(new byte[]{0x00, 0x00}); //reserved + + AudioQuality l = new AudioQuality(); + l.fourCC = getFormat(ase); + l.bitrate = dtsSpecificBox.getAvgBitRate(); + l.audioTag = 65534; + l.samplingRate = dtsSpecificBox.getDTSSamplingFrequency(); + l.channels = getNumChannelsAndMask(dtsSpecificBox)[0]; + l.bitPerSample = 16; + l.packetSize = track.getSamples().get(0).limit(); //assuming all are same size + l.codecPrivateData = Hex.encodeHex(waveformatex.array()) + Hex.encodeHex(dtsCodecPrivateData.array()); + return l; + + } + + /* dwChannelMask + L SPEAKER_FRONT_LEFT 0x00000001 + R SPEAKER_FRONT_RIGHT 0x00000002 + C SPEAKER_FRONT_CENTER 0x00000004 + LFE1 SPEAKER_LOW_FREQUENCY 0x00000008 + Ls or Lsr* SPEAKER_BACK_LEFT 0x00000010 + Rs or Rsr* SPEAKER_BACK_RIGHT 0x00000020 + Lc SPEAKER_FRONT_LEFT_OF_CENTER 0x00000040 + Rc SPEAKER_FRONT_RIGHT_OF_CENTER 0x00000080 + Cs SPEAKER_BACK_CENTER 0x00000100 + Lss SPEAKER_SIDE_LEFT 0x00000200 + Rss SPEAKER_SIDE_RIGHT 0x00000400 + Oh SPEAKER_TOP_CENTER 0x00000800 + Lh SPEAKER_TOP_FRONT_LEFT 0x00001000 + Ch SPEAKER_TOP_FRONT_CENTER 0x00002000 + Rh SPEAKER_TOP_FRONT_RIGHT 0x00004000 + Lhr SPEAKER_TOP_BACK_LEFT 0x00008000 + Chf SPEAKER_TOP_BACK_CENTER 0x00010000 + Rhr SPEAKER_TOP_BACK_RIGHT 0x00020000 + SPEAKER_RESERVED 0x80000000 + + * if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + */ + private int[] getNumChannelsAndMask(DTSSpecificBox dtsSpecificBox) { + final int channelLayout = dtsSpecificBox.getChannelLayout(); + int numChannels = 0; + int dwChannelMask = 0; + if ((channelLayout & 0x0001) == 0x0001) { + //0001h Center in front of listener 1 + numChannels += 1; + dwChannelMask |= 0x00000004; //SPEAKER_FRONT_CENTER + } + if ((channelLayout & 0x0002) == 0x0002) { + //0002h Left/Right in front 2 + numChannels += 2; + dwChannelMask |= 0x00000001; //SPEAKER_FRONT_LEFT + dwChannelMask |= 0x00000002; //SPEAKER_FRONT_RIGHT + } + if ((channelLayout & 0x0004) == 0x0004) { + //0004h Left/Right surround on side in rear 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0008) == 0x0008) { + //0008h Low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x0010) == 0x0010) { + //0010h Center surround in rear 1 + numChannels += 1; + dwChannelMask |= 0x00000100; //SPEAKER_BACK_CENTER + } + if ((channelLayout & 0x0020) == 0x0020) { + //0020h Left/Right height in front 2 + numChannels += 2; + dwChannelMask |= 0x00001000; //SPEAKER_TOP_FRONT_LEFT + dwChannelMask |= 0x00004000; //SPEAKER_TOP_FRONT_RIGHT + } + if ((channelLayout & 0x0040) == 0x0040) { + //0040h Left/Right surround in rear 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0080) == 0x0080) { + //0080h Center Height in front 1 + numChannels += 1; + dwChannelMask |= 0x00002000; //SPEAKER_TOP_FRONT_CENTER + } + if ((channelLayout & 0x0100) == 0x0100) { + //0100h Over the listener’s head 1 + numChannels += 1; + dwChannelMask |= 0x00000800; //SPEAKER_TOP_CENTER + } + if ((channelLayout & 0x0200) == 0x0200) { + //0200h Between left/right and center in front 2 + numChannels += 2; + dwChannelMask |= 0x00000040; //SPEAKER_FRONT_LEFT_OF_CENTER + dwChannelMask |= 0x00000080; //SPEAKER_FRONT_RIGHT_OF_CENTER + } + if ((channelLayout & 0x0400) == 0x0400) { + //0400h Left/Right on side in front 2 + numChannels += 2; + dwChannelMask |= 0x00000200; //SPEAKER_SIDE_LEFT + dwChannelMask |= 0x00000400; //SPEAKER_SIDE_RIGHT + } + if ((channelLayout & 0x0800) == 0x0800) { + //0800h Left/Right surround on side 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x1000) == 0x1000) { + //1000h Second low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x2000) == 0x2000) { + //2000h Left/Right height on side 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x4000) == 0x4000) { + //4000h Center height in rear 1 + numChannels += 1; + dwChannelMask |= 0x00010000; //SPEAKER_TOP_BACK_CENTER + } + if ((channelLayout & 0x8000) == 0x8000) { + //8000h Left/Right height in rear 2 + numChannels += 2; + dwChannelMask |= 0x00008000; //SPEAKER_TOP_BACK_LEFT + dwChannelMask |= 0x00020000; //SPEAKER_TOP_BACK_RIGHT + } + if ((channelLayout & 0x10000) == 0x10000) { + //10000h Center below in front + numChannels += 1; + } + if ((channelLayout & 0x20000) == 0x20000) { + //20000h Left/Right below in front + numChannels += 2; + } + return new int[]{numChannels, dwChannelMask}; + } + + private String getAudioCodecPrivateData(AudioSpecificConfig audioSpecificConfig) { + byte[] configByteArray = audioSpecificConfig.getConfigBytes(); + return Hex.encodeHex(configByteArray); + } + + private VideoQuality getVideoQuality(Track track, VisualSampleEntry vse) { + VideoQuality l; + if ("avc1".equals(getFormat(vse))) { + AvcConfigurationBox avcConfigurationBox = vse.getBoxes(AvcConfigurationBox.class).get(0); + l = new VideoQuality(); + l.bitrate = getBitrate(track); + l.codecPrivateData = Hex.encodeHex(getAvcCodecPrivateData(avcConfigurationBox)); + l.fourCC = "AVC1"; + l.width = vse.getWidth(); + l.height = vse.getHeight(); + l.nalLength = avcConfigurationBox.getLengthSizeMinusOne() + 1; + } else { + throw new InternalError("I don't know how to handle video of type " + getFormat(vse)); + } + return l; + } + + private byte[] getAvcCodecPrivateData(AvcConfigurationBox avcConfigurationBox) { + List<byte[]> sps = avcConfigurationBox.getSequenceParameterSets(); + List<byte[]> pps = avcConfigurationBox.getPictureParameterSets(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + baos.write(new byte[]{0, 0, 0, 1}); + + for (byte[] sp : sps) { + baos.write(sp); + } + baos.write(new byte[]{0, 0, 0, 1}); + for (byte[] pp : pps) { + baos.write(pp); + } + } catch (IOException ex) { + throw new RuntimeException("ByteArrayOutputStream do not throw IOException ?!?!?"); + } + return baos.toByteArray(); + } + + private class DependentSubstreamMask { + private byte dWChannelMaskFirstByte; + private byte dWChannelMaskSecondByte; + private EC3SpecificBox.Entry entry; + + public DependentSubstreamMask(byte dWChannelMaskFirstByte, byte dWChannelMaskSecondByte, EC3SpecificBox.Entry entry) { + this.dWChannelMaskFirstByte = dWChannelMaskFirstByte; + this.dWChannelMaskSecondByte = dWChannelMaskSecondByte; + this.entry = entry; + } + + public byte getdWChannelMaskFirstByte() { + return dWChannelMaskFirstByte; + } + + public byte getdWChannelMaskSecondByte() { + return dWChannelMaskSecondByte; + } + + public DependentSubstreamMask process() { + switch (entry.chan_loc) { + case 0: + dWChannelMaskFirstByte |= 0x3; + break; + case 1: + dWChannelMaskFirstByte |= 0xC; + break; + case 2: + dWChannelMaskSecondByte |= 0x80; + break; + case 3: + dWChannelMaskSecondByte |= 0x8; + break; + case 6: + dWChannelMaskSecondByte |= 0x5; + break; + case 7: + dWChannelMaskSecondByte |= 0x2; + break; + } + return this; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatPackageWriterImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatPackageWriterImpl.java new file mode 100644 index 0000000..3e3847c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/FlatPackageWriterImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.SoundMediaHeaderBox; +import com.coremedia.iso.boxes.VideoMediaHeaderBox; +import com.coremedia.iso.boxes.fragment.MovieFragmentBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.*; +import com.googlecode.mp4parser.authoring.tracks.ChangeTimeScaleTrack; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.Iterator; +import java.util.logging.Logger; + +public class FlatPackageWriterImpl implements PackageWriter { + private static Logger LOG = Logger.getLogger(FlatPackageWriterImpl.class.getName()); + long timeScale = 10000000; + + private File outputDirectory; + private boolean debugOutput; + private FragmentedMp4Builder ismvBuilder; + ManifestWriter manifestWriter; + + public FlatPackageWriterImpl() { + ismvBuilder = new FragmentedMp4Builder(); + FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl(); + ismvBuilder.setIntersectionFinder(intersectionFinder); + manifestWriter = new FlatManifestWriterImpl(intersectionFinder); + } + + /** + * Creates a factory for a smooth streaming package. A smooth streaming package is + * a collection of files that can be served by a webserver as a smooth streaming + * stream. + * @param minFragmentDuration the smallest allowable duration of a fragment (0 == no restriction). + */ + public FlatPackageWriterImpl(int minFragmentDuration) { + ismvBuilder = new FragmentedMp4Builder(); + FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl(minFragmentDuration); + ismvBuilder.setIntersectionFinder(intersectionFinder); + manifestWriter = new FlatManifestWriterImpl(intersectionFinder); + } + + public void setOutputDirectory(File outputDirectory) { + assert outputDirectory.isDirectory(); + this.outputDirectory = outputDirectory; + + } + + public void setDebugOutput(boolean debugOutput) { + this.debugOutput = debugOutput; + } + + public void setIsmvBuilder(FragmentedMp4Builder ismvBuilder) { + this.ismvBuilder = ismvBuilder; + this.manifestWriter = new FlatManifestWriterImpl(ismvBuilder.getFragmentIntersectionFinder()); + } + + public void setManifestWriter(ManifestWriter manifestWriter) { + this.manifestWriter = manifestWriter; + } + + /** + * Writes the movie given as <code>qualities</code> flattened into the + * <code>outputDirectory</code>. + * + * @param source the source movie with all qualities + * @throws IOException + */ + public void write(Movie source) throws IOException { + + if (debugOutput) { + outputDirectory.mkdirs(); + DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder(); + IsoFile muxed = defaultMp4Builder.build(source); + File muxedFile = new File(outputDirectory, "debug_1_muxed.mp4"); + FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile); + muxed.getBox(muxedFileOutputStream.getChannel()); + muxedFileOutputStream.close(); + } + Movie cleanedSource = removeUnknownTracks(source); + Movie movieWithAdjustedTimescale = correctTimescale(cleanedSource); + + if (debugOutput) { + DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder(); + IsoFile muxed = defaultMp4Builder.build(movieWithAdjustedTimescale); + File muxedFile = new File(outputDirectory, "debug_2_timescale.mp4"); + FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile); + muxed.getBox(muxedFileOutputStream.getChannel()); + muxedFileOutputStream.close(); + } + IsoFile isoFile = ismvBuilder.build(movieWithAdjustedTimescale); + if (debugOutput) { + File allQualities = new File(outputDirectory, "debug_3_fragmented.mp4"); + FileOutputStream allQualis = new FileOutputStream(allQualities); + isoFile.getBox(allQualis.getChannel()); + allQualis.close(); + } + + + for (Track track : movieWithAdjustedTimescale.getTracks()) { + String bitrate = Long.toString(manifestWriter.getBitrate(track)); + long trackId = track.getTrackMetaData().getTrackId(); + Iterator<Box> boxIt = isoFile.getBoxes().iterator(); + File mediaOutDir; + if (track.getMediaHeaderBox() instanceof SoundMediaHeaderBox) { + mediaOutDir = new File(outputDirectory, "audio"); + + } else if (track.getMediaHeaderBox() instanceof VideoMediaHeaderBox) { + mediaOutDir = new File(outputDirectory, "video"); + } else { + System.err.println("Skipping Track with handler " + track.getHandler() + " and " + track.getMediaHeaderBox().getClass().getSimpleName()); + continue; + } + File bitRateOutputDir = new File(mediaOutDir, bitrate); + bitRateOutputDir.mkdirs(); + LOG.finer("Created : " + bitRateOutputDir.getCanonicalPath()); + + long[] fragmentTimes = manifestWriter.calculateFragmentDurations(track, movieWithAdjustedTimescale); + long startTime = 0; + int currentFragment = 0; + while (boxIt.hasNext()) { + Box b = boxIt.next(); + if (b instanceof MovieFragmentBox) { + assert ((MovieFragmentBox) b).getTrackCount() == 1; + if (((MovieFragmentBox) b).getTrackNumbers()[0] == trackId) { + FileOutputStream fos = new FileOutputStream(new File(bitRateOutputDir, Long.toString(startTime))); + startTime += fragmentTimes[currentFragment++]; + FileChannel fc = fos.getChannel(); + Box mdat = boxIt.next(); + assert mdat.getType().equals("mdat"); + b.getBox(fc); // moof + mdat.getBox(fc); // mdat + fc.truncate(fc.position()); + fc.close(); + } + } + + } + } + FileWriter fw = new FileWriter(new File(outputDirectory, "Manifest")); + fw.write(manifestWriter.getManifest(movieWithAdjustedTimescale)); + fw.close(); + + } + + private Movie removeUnknownTracks(Movie source) { + Movie nuMovie = new Movie(); + for (Track track : source.getTracks()) { + if ("vide".equals(track.getHandler()) || "soun".equals(track.getHandler())) { + nuMovie.addTrack(track); + } else { + LOG.fine("Removed track " + track); + } + } + return nuMovie; + } + + + /** + * Returns a new <code>Movie</code> in that all tracks have the timescale 10000000. CTS & DTS are modified + * in a way that even with more than one framerate the fragments exactly begin at the same time. + * + * @param movie + * @return a movie with timescales suitable for smooth streaming manifests + */ + public Movie correctTimescale(Movie movie) { + Movie nuMovie = new Movie(); + for (Track track : movie.getTracks()) { + nuMovie.addTrack(new ChangeTimeScaleTrack(track, timeScale, ismvBuilder.getFragmentIntersectionFinder().sampleNumbers(track, movie))); + } + return nuMovie; + + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/ManifestWriter.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/ManifestWriter.java new file mode 100644 index 0000000..2b2ba7d --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/ManifestWriter.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + + +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; + +public interface ManifestWriter { + String getManifest(Movie inputs) throws IOException; + + long getBitrate(Track track); + + long[] calculateFragmentDurations(Track track, Movie movie); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/PackageWriter.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/PackageWriter.java new file mode 100644 index 0000000..0d97fc5 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/PackageWriter.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +import com.googlecode.mp4parser.authoring.Movie; + +import java.io.IOException; + +/** + * Writes the whole package. + */ +public interface PackageWriter { + public void write(Movie qualities) throws IOException; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/VideoQuality.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/VideoQuality.java new file mode 100644 index 0000000..4a70e47 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/adaptivestreaming/VideoQuality.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.googlecode.mp4parser.authoring.adaptivestreaming; + +class VideoQuality { + long bitrate; + String fourCC; + int width; + int height; + String codecPrivateData; + int nalLength; +} |