diff options
Diffstat (limited to 'isoparser/src/main/java/com/googlecode/mp4parser/authoring')
82 files changed, 15945 insertions, 0 deletions
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/all-wcprops new file mode 100644 index 0000000..89054c9 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/all-wcprops @@ -0,0 +1,41 @@ +K 25 +svn:wc:ra_dav:version-url +V 82 +/svn/!svn/ver/776/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring +END +Movie.java +K 25 +svn:wc:ra_dav:version-url +V 93 +/svn/!svn/ver/514/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Movie.java +END +Track.java +K 25 +svn:wc:ra_dav:version-url +V 93 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Track.java +END +TrackMetaData.java +K 25 +svn:wc:ra_dav:version-url +V 101 +/svn/!svn/ver/745/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/TrackMetaData.java +END +Mp4TrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 100 +/svn/!svn/ver/765/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Mp4TrackImpl.java +END +AbstractTrack.java +K 25 +svn:wc:ra_dav:version-url +V 101 +/svn/!svn/ver/418/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/AbstractTrack.java +END +DateHelper.java +K 25 +svn:wc:ra_dav:version-url +V 98 +/svn/!svn/ver/418/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/DateHelper.java +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/entries new file mode 100644 index 0000000..7d2b29e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/entries @@ -0,0 +1,244 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring +http://mp4parser.googlecode.com/svn + + + +2012-09-10T14:34:23.574807Z +776 +sebastian.annies@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +Movie.java +file + + + + +2012-09-14T17:27:50.517219Z +e3a56133cfdfacb92ed0a54177e847b4 +2012-04-22T10:09:06.632613Z +514 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +2558 + +container +dir + +Track.java +file + + + + +2012-09-14T17:27:50.517219Z +9537fa79b71fe26727e56e84e94bbdb8 +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +1607 + +TrackMetaData.java +file + + + + +2012-09-14T17:27:50.527219Z +cbb770cca0ee421026eec0a2f40d2376 +2012-08-14T19:18:50.777750Z +745 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +2948 + +builder +dir + +adaptivestreaming +dir + +tracks +dir + +Mp4TrackImpl.java +file + + + + +2012-09-14T17:27:50.527219Z +c57930172e9d0da9e881d8dc8ecf2924 +2012-08-29T08:26:56.932482Z +765 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +10958 + +AbstractTrack.java +file + + + + +2012-09-14T17:27:50.527219Z +973f4f354fb6f575dd1a0c8a68d54653 +2012-03-11T20:54:45.638478Z +418 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +1481 + +DateHelper.java +file + + + + +2012-09-14T17:27:50.527219Z +765e3f37d7bb369f569aa91e326a90b8 +2012-03-11T20:54:45.638478Z +418 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +1349 + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/AbstractTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/AbstractTrack.java.svn-base new file mode 100644 index 0000000..fb0e224 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/AbstractTrack.java.svn-base @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * + */ +public abstract class AbstractTrack implements Track { + private boolean enabled = true; + private boolean inMovie = true; + private boolean inPreview = true; + private boolean inPoster = true; + + public boolean isEnabled() { + return enabled; + } + + public boolean isInMovie() { + return inMovie; + } + + public boolean isInPreview() { + return inPreview; + } + + public boolean isInPoster() { + return inPoster; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setInMovie(boolean inMovie) { + this.inMovie = inMovie; + } + + public void setInPreview(boolean inPreview) { + this.inPreview = inPreview; + } + + public void setInPoster(boolean inPoster) { + this.inPoster = inPoster; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/DateHelper.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/DateHelper.java.svn-base new file mode 100644 index 0000000..0252859 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/DateHelper.java.svn-base @@ -0,0 +1,44 @@ +/* + * 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; + +import java.util.Date; + +/** + * Converts ISO Dates (seconds since 1/1/1904) to Date and vice versa. + */ +public class DateHelper { + /** + * Converts a long value with seconds since 1/1/1904 to Date. + * + * @param secondsSince seconds since 1/1/1904 + * @return date the corresponding <code>Date</code> + */ + static public Date convert(long secondsSince) { + return new Date((secondsSince - 2082844800L) * 1000L); + } + + + /** + * Converts a date as long to a mac date as long + * + * @param date date to convert + * @return date in mac format + */ + static public long convert(Date date) { + return (date.getTime() / 1000L) + 2082844800L; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Movie.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Movie.java.svn-base new file mode 100644 index 0000000..0658682 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Movie.java.svn-base @@ -0,0 +1,91 @@ +/* + * 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; + +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +public class Movie { + List<Track> tracks = new LinkedList<Track>(); + + public List<Track> getTracks() { + return tracks; + } + + public void setTracks(List<Track> tracks) { + this.tracks = tracks; + } + + public void addTrack(Track nuTrack) { + // do some checking + // perhaps the movie needs to get longer! + if (getTrackByTrackId(nuTrack.getTrackMetaData().getTrackId()) != null) { + // We already have a track with that trackId. Create a new one + nuTrack.getTrackMetaData().setTrackId(getNextTrackId()); + } + tracks.add(nuTrack); + } + + + @Override + public String toString() { + String s = "Movie{ "; + for (Track track : tracks) { + s += "track_" + track.getTrackMetaData().getTrackId() + " (" + track.getHandler() + ") "; + } + + s += '}'; + return s; + } + + public long getNextTrackId() { + long nextTrackId = 0; + for (Track track : tracks) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + return ++nextTrackId; + } + + + public Track getTrackByTrackId(long trackId) { + for (Track track : tracks) { + if (track.getTrackMetaData().getTrackId() == trackId) { + return track; + } + } + return null; + } + + + public long getTimescale() { + long timescale = this.getTracks().iterator().next().getTrackMetaData().getTimescale(); + for (Track track : this.getTracks()) { + timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); + } + return timescale; + } + + public static long gcd(long a, long b) { + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Mp4TrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Mp4TrackImpl.java.svn-base new file mode 100644 index 0000000..3bff1a5 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Mp4TrackImpl.java.svn-base @@ -0,0 +1,219 @@ +/* + * 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; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.fragment.*; +import com.coremedia.iso.boxes.mdat.SampleList; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Represents a single track of an MP4 file. + */ +public class Mp4TrackImpl extends AbstractTrack { + private List<ByteBuffer> samples; + private SampleDescriptionBox sampleDescriptionBox; + private List<TimeToSampleBox.Entry> decodingTimeEntries; + private List<CompositionTimeToSample.Entry> compositionTimeEntries; + private long[] syncSamples = new long[0]; + private List<SampleDependencyTypeBox.Entry> sampleDependencies; + private TrackMetaData trackMetaData = new TrackMetaData(); + private String handler; + private AbstractMediaHeaderBox mihd; + + public Mp4TrackImpl(TrackBox trackBox) { + final long trackId = trackBox.getTrackHeaderBox().getTrackId(); + samples = new SampleList(trackBox); + SampleTableBox stbl = trackBox.getMediaBox().getMediaInformationBox().getSampleTableBox(); + handler = trackBox.getMediaBox().getHandlerBox().getHandlerType(); + + mihd = trackBox.getMediaBox().getMediaInformationBox().getMediaHeaderBox(); + decodingTimeEntries = new LinkedList<TimeToSampleBox.Entry>(); + compositionTimeEntries = new LinkedList<CompositionTimeToSample.Entry>(); + sampleDependencies = new LinkedList<SampleDependencyTypeBox.Entry>(); + + decodingTimeEntries.addAll(stbl.getTimeToSampleBox().getEntries()); + if (stbl.getCompositionTimeToSample() != null) { + compositionTimeEntries.addAll(stbl.getCompositionTimeToSample().getEntries()); + } + if (stbl.getSampleDependencyTypeBox() != null) { + sampleDependencies.addAll(stbl.getSampleDependencyTypeBox().getEntries()); + } + if (stbl.getSyncSampleBox() != null) { + syncSamples = stbl.getSyncSampleBox().getSampleNumber(); + } + + + sampleDescriptionBox = stbl.getSampleDescriptionBox(); + final List<MovieExtendsBox> movieExtendsBoxes = trackBox.getParent().getBoxes(MovieExtendsBox.class); + if (movieExtendsBoxes.size() > 0) { + for (MovieExtendsBox mvex : movieExtendsBoxes) { + final List<TrackExtendsBox> trackExtendsBoxes = mvex.getBoxes(TrackExtendsBox.class); + for (TrackExtendsBox trex : trackExtendsBoxes) { + if (trex.getTrackId() == trackId) { + List<Long> syncSampleList = new LinkedList<Long>(); + + long sampleNumber = 1; + for (MovieFragmentBox movieFragmentBox : trackBox.getIsoFile().getBoxes(MovieFragmentBox.class)) { + List<TrackFragmentBox> trafs = movieFragmentBox.getBoxes(TrackFragmentBox.class); + for (TrackFragmentBox traf : trafs) { + if (traf.getTrackFragmentHeaderBox().getTrackId() == trackId) { + List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); + for (TrackRunBox trun : truns) { + final TrackFragmentHeaderBox tfhd = ((TrackFragmentBox) trun.getParent()).getTrackFragmentHeaderBox(); + boolean first = true; + for (TrackRunBox.Entry entry : trun.getEntries()) { + if (trun.isSampleDurationPresent()) { + if (decodingTimeEntries.size() == 0 || + decodingTimeEntries.get(decodingTimeEntries.size() - 1).getDelta() != entry.getSampleDuration()) { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, entry.getSampleDuration())); + } else { + TimeToSampleBox.Entry e = decodingTimeEntries.get(decodingTimeEntries.size() - 1); + e.setCount(e.getCount() + 1); + } + } else { + if (tfhd.hasDefaultSampleDuration()) { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, tfhd.getDefaultSampleDuration())); + } else { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, trex.getDefaultSampleDuration())); + } + } + + if (trun.isSampleCompositionTimeOffsetPresent()) { + if (compositionTimeEntries.size() == 0 || + compositionTimeEntries.get(compositionTimeEntries.size() - 1).getOffset() != entry.getSampleCompositionTimeOffset()) { + compositionTimeEntries.add(new CompositionTimeToSample.Entry(1, l2i(entry.getSampleCompositionTimeOffset()))); + } else { + CompositionTimeToSample.Entry e = compositionTimeEntries.get(compositionTimeEntries.size() - 1); + e.setCount(e.getCount() + 1); + } + } + final SampleFlags sampleFlags; + if (trun.isSampleFlagsPresent()) { + sampleFlags = entry.getSampleFlags(); + } else { + if (first && trun.isFirstSampleFlagsPresent()) { + sampleFlags = trun.getFirstSampleFlags(); + } else { + if (tfhd.hasDefaultSampleFlags()) { + sampleFlags = tfhd.getDefaultSampleFlags(); + } else { + sampleFlags = trex.getDefaultSampleFlags(); + } + } + } + if (sampleFlags != null && !sampleFlags.isSampleIsDifferenceSample()) { + //iframe + syncSampleList.add(sampleNumber); + } + sampleNumber++; + first = false; + } + } + } + } + } + // Warning: Crappy code + long[] oldSS = syncSamples; + syncSamples = new long[syncSamples.length + syncSampleList.size()]; + System.arraycopy(oldSS, 0, syncSamples, 0, oldSS.length); + final Iterator<Long> iterator = syncSampleList.iterator(); + int i = oldSS.length; + while (iterator.hasNext()) { + Long syncSampleNumber = iterator.next(); + syncSamples[i++] = syncSampleNumber; + } + } + } + } + } + MediaHeaderBox mdhd = trackBox.getMediaBox().getMediaHeaderBox(); + TrackHeaderBox tkhd = trackBox.getTrackHeaderBox(); + + setEnabled(tkhd.isEnabled()); + setInMovie(tkhd.isInMovie()); + setInPoster(tkhd.isInPoster()); + setInPreview(tkhd.isInPreview()); + + trackMetaData.setTrackId(tkhd.getTrackId()); + trackMetaData.setCreationTime(DateHelper.convert(mdhd.getCreationTime())); + trackMetaData.setLanguage(mdhd.getLanguage()); +/* System.err.println(mdhd.getModificationTime()); + System.err.println(DateHelper.convert(mdhd.getModificationTime())); + System.err.println(DateHelper.convert(DateHelper.convert(mdhd.getModificationTime()))); + System.err.println(DateHelper.convert(DateHelper.convert(DateHelper.convert(mdhd.getModificationTime()))));*/ + + trackMetaData.setModificationTime(DateHelper.convert(mdhd.getModificationTime())); + trackMetaData.setTimescale(mdhd.getTimescale()); + trackMetaData.setHeight(tkhd.getHeight()); + trackMetaData.setWidth(tkhd.getWidth()); + trackMetaData.setLayer(tkhd.getLayer()); + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return decodingTimeEntries; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return compositionTimeEntries; + } + + public long[] getSyncSamples() { + return syncSamples; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return sampleDependencies; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return handler; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return mihd; + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + @Override + public String toString() { + return "Mp4TrackImpl{" + + "handler='" + handler + '\'' + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Track.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Track.java.svn-base new file mode 100644 index 0000000..1f4b363 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/Track.java.svn-base @@ -0,0 +1,60 @@ +/* + * 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; + +import com.coremedia.iso.boxes.*; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Represents a Track. A track is a timed sequence of related samples. + * <p/> + * <b>NOTE: </b><br/ + * For media data, a track corresponds to a sequence of images or sampled audio; for hint tracks, a track + * corresponds to a streaming channel. + */ +public interface Track { + + SampleDescriptionBox getSampleDescriptionBox(); + + List<TimeToSampleBox.Entry> getDecodingTimeEntries(); + + List<CompositionTimeToSample.Entry> getCompositionTimeEntries(); + + long[] getSyncSamples(); + + List<SampleDependencyTypeBox.Entry> getSampleDependencies(); + + TrackMetaData getTrackMetaData(); + + String getHandler(); + + boolean isEnabled(); + + boolean isInMovie(); + + boolean isInPreview(); + + boolean isInPoster(); + + List<ByteBuffer> getSamples(); + + public Box getMediaHeaderBox(); + + public SubSampleInformationBox getSubsampleInformationBox(); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/TrackMetaData.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/TrackMetaData.java.svn-base new file mode 100644 index 0000000..c262309 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/.svn/text-base/TrackMetaData.java.svn-base @@ -0,0 +1,130 @@ +/* + * 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; + +import java.util.Date; + +/** + * + */ +public class TrackMetaData implements Cloneable { + private String language; + private long timescale; + private Date modificationTime = new Date(); + private Date creationTime = new Date(); + private double width; + private double height; + private float volume; + private long trackId = 1; // zero is not allowed + private int group = 0; + + + /** + * specifies the front-to-back ordering of video tracks; tracks with lower + * numbers are closer to the viewer. 0 is the normal value, and -1 would be + * in front of track 0, and so on. + */ + int layer; + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public long getTimescale() { + return timescale; + } + + public void setTimescale(long timescale) { + this.timescale = timescale; + } + + public Date getModificationTime() { + return modificationTime; + } + + public void setModificationTime(Date modificationTime) { + this.modificationTime = modificationTime; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + public long getTrackId() { + return trackId; + } + + public void setTrackId(long trackId) { + this.trackId = trackId; + } + + public int getLayer() { + return layer; + } + + public void setLayer(int layer) { + this.layer = layer; + } + + public float getVolume() { + return volume; + } + + public void setVolume(float volume) { + this.volume = volume; + } + + public int getGroup() { + return group; + } + + public void setGroup(int group) { + this.group = group; + } + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + return null; + } + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/AbstractTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/AbstractTrack.java new file mode 100644 index 0000000..fb0e224 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/AbstractTrack.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * + */ +public abstract class AbstractTrack implements Track { + private boolean enabled = true; + private boolean inMovie = true; + private boolean inPreview = true; + private boolean inPoster = true; + + public boolean isEnabled() { + return enabled; + } + + public boolean isInMovie() { + return inMovie; + } + + public boolean isInPreview() { + return inPreview; + } + + public boolean isInPoster() { + return inPoster; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setInMovie(boolean inMovie) { + this.inMovie = inMovie; + } + + public void setInPreview(boolean inPreview) { + this.inPreview = inPreview; + } + + public void setInPoster(boolean inPoster) { + this.inPoster = inPoster; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/DateHelper.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/DateHelper.java new file mode 100644 index 0000000..0252859 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/DateHelper.java @@ -0,0 +1,44 @@ +/* + * 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; + +import java.util.Date; + +/** + * Converts ISO Dates (seconds since 1/1/1904) to Date and vice versa. + */ +public class DateHelper { + /** + * Converts a long value with seconds since 1/1/1904 to Date. + * + * @param secondsSince seconds since 1/1/1904 + * @return date the corresponding <code>Date</code> + */ + static public Date convert(long secondsSince) { + return new Date((secondsSince - 2082844800L) * 1000L); + } + + + /** + * Converts a date as long to a mac date as long + * + * @param date date to convert + * @return date in mac format + */ + static public long convert(Date date) { + return (date.getTime() / 1000L) + 2082844800L; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Movie.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Movie.java new file mode 100644 index 0000000..0658682 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Movie.java @@ -0,0 +1,91 @@ +/* + * 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; + +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +public class Movie { + List<Track> tracks = new LinkedList<Track>(); + + public List<Track> getTracks() { + return tracks; + } + + public void setTracks(List<Track> tracks) { + this.tracks = tracks; + } + + public void addTrack(Track nuTrack) { + // do some checking + // perhaps the movie needs to get longer! + if (getTrackByTrackId(nuTrack.getTrackMetaData().getTrackId()) != null) { + // We already have a track with that trackId. Create a new one + nuTrack.getTrackMetaData().setTrackId(getNextTrackId()); + } + tracks.add(nuTrack); + } + + + @Override + public String toString() { + String s = "Movie{ "; + for (Track track : tracks) { + s += "track_" + track.getTrackMetaData().getTrackId() + " (" + track.getHandler() + ") "; + } + + s += '}'; + return s; + } + + public long getNextTrackId() { + long nextTrackId = 0; + for (Track track : tracks) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + return ++nextTrackId; + } + + + public Track getTrackByTrackId(long trackId) { + for (Track track : tracks) { + if (track.getTrackMetaData().getTrackId() == trackId) { + return track; + } + } + return null; + } + + + public long getTimescale() { + long timescale = this.getTracks().iterator().next().getTrackMetaData().getTimescale(); + for (Track track : this.getTracks()) { + timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); + } + return timescale; + } + + public static long gcd(long a, long b) { + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Mp4TrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Mp4TrackImpl.java new file mode 100644 index 0000000..3bff1a5 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Mp4TrackImpl.java @@ -0,0 +1,219 @@ +/* + * 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; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.fragment.*; +import com.coremedia.iso.boxes.mdat.SampleList; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Represents a single track of an MP4 file. + */ +public class Mp4TrackImpl extends AbstractTrack { + private List<ByteBuffer> samples; + private SampleDescriptionBox sampleDescriptionBox; + private List<TimeToSampleBox.Entry> decodingTimeEntries; + private List<CompositionTimeToSample.Entry> compositionTimeEntries; + private long[] syncSamples = new long[0]; + private List<SampleDependencyTypeBox.Entry> sampleDependencies; + private TrackMetaData trackMetaData = new TrackMetaData(); + private String handler; + private AbstractMediaHeaderBox mihd; + + public Mp4TrackImpl(TrackBox trackBox) { + final long trackId = trackBox.getTrackHeaderBox().getTrackId(); + samples = new SampleList(trackBox); + SampleTableBox stbl = trackBox.getMediaBox().getMediaInformationBox().getSampleTableBox(); + handler = trackBox.getMediaBox().getHandlerBox().getHandlerType(); + + mihd = trackBox.getMediaBox().getMediaInformationBox().getMediaHeaderBox(); + decodingTimeEntries = new LinkedList<TimeToSampleBox.Entry>(); + compositionTimeEntries = new LinkedList<CompositionTimeToSample.Entry>(); + sampleDependencies = new LinkedList<SampleDependencyTypeBox.Entry>(); + + decodingTimeEntries.addAll(stbl.getTimeToSampleBox().getEntries()); + if (stbl.getCompositionTimeToSample() != null) { + compositionTimeEntries.addAll(stbl.getCompositionTimeToSample().getEntries()); + } + if (stbl.getSampleDependencyTypeBox() != null) { + sampleDependencies.addAll(stbl.getSampleDependencyTypeBox().getEntries()); + } + if (stbl.getSyncSampleBox() != null) { + syncSamples = stbl.getSyncSampleBox().getSampleNumber(); + } + + + sampleDescriptionBox = stbl.getSampleDescriptionBox(); + final List<MovieExtendsBox> movieExtendsBoxes = trackBox.getParent().getBoxes(MovieExtendsBox.class); + if (movieExtendsBoxes.size() > 0) { + for (MovieExtendsBox mvex : movieExtendsBoxes) { + final List<TrackExtendsBox> trackExtendsBoxes = mvex.getBoxes(TrackExtendsBox.class); + for (TrackExtendsBox trex : trackExtendsBoxes) { + if (trex.getTrackId() == trackId) { + List<Long> syncSampleList = new LinkedList<Long>(); + + long sampleNumber = 1; + for (MovieFragmentBox movieFragmentBox : trackBox.getIsoFile().getBoxes(MovieFragmentBox.class)) { + List<TrackFragmentBox> trafs = movieFragmentBox.getBoxes(TrackFragmentBox.class); + for (TrackFragmentBox traf : trafs) { + if (traf.getTrackFragmentHeaderBox().getTrackId() == trackId) { + List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); + for (TrackRunBox trun : truns) { + final TrackFragmentHeaderBox tfhd = ((TrackFragmentBox) trun.getParent()).getTrackFragmentHeaderBox(); + boolean first = true; + for (TrackRunBox.Entry entry : trun.getEntries()) { + if (trun.isSampleDurationPresent()) { + if (decodingTimeEntries.size() == 0 || + decodingTimeEntries.get(decodingTimeEntries.size() - 1).getDelta() != entry.getSampleDuration()) { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, entry.getSampleDuration())); + } else { + TimeToSampleBox.Entry e = decodingTimeEntries.get(decodingTimeEntries.size() - 1); + e.setCount(e.getCount() + 1); + } + } else { + if (tfhd.hasDefaultSampleDuration()) { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, tfhd.getDefaultSampleDuration())); + } else { + decodingTimeEntries.add(new TimeToSampleBox.Entry(1, trex.getDefaultSampleDuration())); + } + } + + if (trun.isSampleCompositionTimeOffsetPresent()) { + if (compositionTimeEntries.size() == 0 || + compositionTimeEntries.get(compositionTimeEntries.size() - 1).getOffset() != entry.getSampleCompositionTimeOffset()) { + compositionTimeEntries.add(new CompositionTimeToSample.Entry(1, l2i(entry.getSampleCompositionTimeOffset()))); + } else { + CompositionTimeToSample.Entry e = compositionTimeEntries.get(compositionTimeEntries.size() - 1); + e.setCount(e.getCount() + 1); + } + } + final SampleFlags sampleFlags; + if (trun.isSampleFlagsPresent()) { + sampleFlags = entry.getSampleFlags(); + } else { + if (first && trun.isFirstSampleFlagsPresent()) { + sampleFlags = trun.getFirstSampleFlags(); + } else { + if (tfhd.hasDefaultSampleFlags()) { + sampleFlags = tfhd.getDefaultSampleFlags(); + } else { + sampleFlags = trex.getDefaultSampleFlags(); + } + } + } + if (sampleFlags != null && !sampleFlags.isSampleIsDifferenceSample()) { + //iframe + syncSampleList.add(sampleNumber); + } + sampleNumber++; + first = false; + } + } + } + } + } + // Warning: Crappy code + long[] oldSS = syncSamples; + syncSamples = new long[syncSamples.length + syncSampleList.size()]; + System.arraycopy(oldSS, 0, syncSamples, 0, oldSS.length); + final Iterator<Long> iterator = syncSampleList.iterator(); + int i = oldSS.length; + while (iterator.hasNext()) { + Long syncSampleNumber = iterator.next(); + syncSamples[i++] = syncSampleNumber; + } + } + } + } + } + MediaHeaderBox mdhd = trackBox.getMediaBox().getMediaHeaderBox(); + TrackHeaderBox tkhd = trackBox.getTrackHeaderBox(); + + setEnabled(tkhd.isEnabled()); + setInMovie(tkhd.isInMovie()); + setInPoster(tkhd.isInPoster()); + setInPreview(tkhd.isInPreview()); + + trackMetaData.setTrackId(tkhd.getTrackId()); + trackMetaData.setCreationTime(DateHelper.convert(mdhd.getCreationTime())); + trackMetaData.setLanguage(mdhd.getLanguage()); +/* System.err.println(mdhd.getModificationTime()); + System.err.println(DateHelper.convert(mdhd.getModificationTime())); + System.err.println(DateHelper.convert(DateHelper.convert(mdhd.getModificationTime()))); + System.err.println(DateHelper.convert(DateHelper.convert(DateHelper.convert(mdhd.getModificationTime()))));*/ + + trackMetaData.setModificationTime(DateHelper.convert(mdhd.getModificationTime())); + trackMetaData.setTimescale(mdhd.getTimescale()); + trackMetaData.setHeight(tkhd.getHeight()); + trackMetaData.setWidth(tkhd.getWidth()); + trackMetaData.setLayer(tkhd.getLayer()); + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return decodingTimeEntries; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return compositionTimeEntries; + } + + public long[] getSyncSamples() { + return syncSamples; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return sampleDependencies; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return handler; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return mihd; + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + @Override + public String toString() { + return "Mp4TrackImpl{" + + "handler='" + handler + '\'' + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Track.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Track.java new file mode 100644 index 0000000..1f4b363 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/Track.java @@ -0,0 +1,60 @@ +/* + * 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; + +import com.coremedia.iso.boxes.*; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Represents a Track. A track is a timed sequence of related samples. + * <p/> + * <b>NOTE: </b><br/ + * For media data, a track corresponds to a sequence of images or sampled audio; for hint tracks, a track + * corresponds to a streaming channel. + */ +public interface Track { + + SampleDescriptionBox getSampleDescriptionBox(); + + List<TimeToSampleBox.Entry> getDecodingTimeEntries(); + + List<CompositionTimeToSample.Entry> getCompositionTimeEntries(); + + long[] getSyncSamples(); + + List<SampleDependencyTypeBox.Entry> getSampleDependencies(); + + TrackMetaData getTrackMetaData(); + + String getHandler(); + + boolean isEnabled(); + + boolean isInMovie(); + + boolean isInPreview(); + + boolean isInPoster(); + + List<ByteBuffer> getSamples(); + + public Box getMediaHeaderBox(); + + public SubSampleInformationBox getSubsampleInformationBox(); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/TrackMetaData.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/TrackMetaData.java new file mode 100644 index 0000000..c262309 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/TrackMetaData.java @@ -0,0 +1,130 @@ +/* + * 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; + +import java.util.Date; + +/** + * + */ +public class TrackMetaData implements Cloneable { + private String language; + private long timescale; + private Date modificationTime = new Date(); + private Date creationTime = new Date(); + private double width; + private double height; + private float volume; + private long trackId = 1; // zero is not allowed + private int group = 0; + + + /** + * specifies the front-to-back ordering of video tracks; tracks with lower + * numbers are closer to the viewer. 0 is the normal value, and -1 would be + * in front of track 0, and so on. + */ + int layer; + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public long getTimescale() { + return timescale; + } + + public void setTimescale(long timescale) { + this.timescale = timescale; + } + + public Date getModificationTime() { + return modificationTime; + } + + public void setModificationTime(Date modificationTime) { + this.modificationTime = modificationTime; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + public long getTrackId() { + return trackId; + } + + public void setTrackId(long trackId) { + this.trackId = trackId; + } + + public int getLayer() { + return layer; + } + + public void setLayer(int layer) { + this.layer = layer; + } + + public float getVolume() { + return volume; + } + + public void setVolume(float volume) { + this.volume = volume; + } + + public int getGroup() { + return group; + } + + public void setGroup(int group) { + this.group = group; + } + + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + return null; + } + } + +} 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; +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/all-wcprops new file mode 100644 index 0000000..9204edf --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/all-wcprops @@ -0,0 +1,47 @@ +K 25 +svn:wc:ra_dav:version-url +V 90 +/svn/!svn/ver/776/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder +END +FragmentIntersectionFinder.java +K 25 +svn:wc:ra_dav:version-url +V 122 +/svn/!svn/ver/455/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentIntersectionFinder.java +END +TwoSecondIntersectionFinder.java +K 25 +svn:wc:ra_dav:version-url +V 123 +/svn/!svn/ver/658/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/TwoSecondIntersectionFinder.java +END +FragmentedMp4Builder.java +K 25 +svn:wc:ra_dav:version-url +V 116 +/svn/!svn/ver/707/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentedMp4Builder.java +END +Mp4Builder.java +K 25 +svn:wc:ra_dav:version-url +V 106 +/svn/!svn/ver/672/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/Mp4Builder.java +END +SyncSampleIntersectFinderImpl.java +K 25 +svn:wc:ra_dav:version-url +V 125 +/svn/!svn/ver/774/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/SyncSampleIntersectFinderImpl.java +END +DefaultMp4Builder.java +K 25 +svn:wc:ra_dav:version-url +V 113 +/svn/!svn/ver/776/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java +END +ByteBufferHelper.java +K 25 +svn:wc:ra_dav:version-url +V 112 +/svn/!svn/ver/530/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/ByteBufferHelper.java +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/entries new file mode 100644 index 0000000..2c6e266 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/entries @@ -0,0 +1,266 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder +http://mp4parser.googlecode.com/svn + + + +2012-09-10T14:34:23.574807Z +776 +sebastian.annies@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +FragmentIntersectionFinder.java +file + + + + +2012-09-14T17:27:50.237215Z +979df582987d3797c42ae315b3d2555a +2012-04-10T10:20:46.357991Z +455 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +1160 + +TwoSecondIntersectionFinder.java +file + + + + +2012-09-14T17:27:50.237215Z +47aa683919ce24da51b82236e8f27426 +2012-06-06T10:36:10.590512Z +658 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +2817 + +FragmentedMp4Builder.java +file + + + + +2012-09-14T17:27:50.237215Z +f81cbeb22aee5ab0405ad38e67b9e4e1 +2012-07-10T16:32:53.757675Z +707 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +31005 + +Mp4Builder.java +file + + + + +2012-09-14T17:27:50.237215Z +ea548a5ace2a4480472b90d9f820bceb +2012-06-11T22:10:18.183835Z +672 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +1156 + +SyncSampleIntersectFinderImpl.java +file + + + + +2012-09-14T17:27:50.237215Z +d49823bb95a5920f2df6899195c28a72 +2012-09-06T12:33:39.419429Z +774 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +14915 + +DefaultMp4Builder.java +file + + + + +2012-09-14T17:27:50.237215Z +15d93ea29e24e257604301ad5e73d623 +2012-09-10T14:34:23.574807Z +776 +sebastian.annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +22096 + +ByteBufferHelper.java +file + + + + +2012-09-14T17:27:50.237215Z +a103511eeddf1e0d1962704ae23ba4a5 +2012-04-27T09:17:25.544414Z +530 +hoemmagnus@gmail.com + + + + + + + + + + + + + + + + + + + + + +2344 + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/ByteBufferHelper.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/ByteBufferHelper.java.svn-base new file mode 100644 index 0000000..ad21b11 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/ByteBufferHelper.java.svn-base @@ -0,0 +1,50 @@ +/* + * 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.builder; + +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Used to merge adjacent byte buffers. + */ +public class ByteBufferHelper { + public static List<ByteBuffer> mergeAdjacentBuffers(List<ByteBuffer> samples) { + ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); + for (ByteBuffer buffer : samples) { + int lastIndex = nuSamples.size() - 1; + if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && + nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { + ByteBuffer oldBuffer = nuSamples.remove(lastIndex); + ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); + // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. + nuSamples.add(nu); + } else if (lastIndex >= 0 && + buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && + nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { + // This can go wrong - but will it? + ByteBuffer oldBuffer = nuSamples.get(lastIndex); + oldBuffer.limit(buffer.limit() + oldBuffer.limit()); + } else { + buffer.rewind(); + nuSamples.add(buffer); + } + } + return nuSamples; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base new file mode 100644 index 0000000..9bd1ca6 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base @@ -0,0 +1,576 @@ +/* + * 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.builder; + +import com.coremedia.iso.BoxParser; +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.IsoTypeWriter; +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.CompositionTimeToSample; +import com.coremedia.iso.boxes.ContainerBox; +import com.coremedia.iso.boxes.DataEntryUrlBox; +import com.coremedia.iso.boxes.DataInformationBox; +import com.coremedia.iso.boxes.DataReferenceBox; +import com.coremedia.iso.boxes.FileTypeBox; +import com.coremedia.iso.boxes.HandlerBox; +import com.coremedia.iso.boxes.MediaBox; +import com.coremedia.iso.boxes.MediaHeaderBox; +import com.coremedia.iso.boxes.MediaInformationBox; +import com.coremedia.iso.boxes.MovieBox; +import com.coremedia.iso.boxes.MovieHeaderBox; +import com.coremedia.iso.boxes.SampleDependencyTypeBox; +import com.coremedia.iso.boxes.SampleSizeBox; +import com.coremedia.iso.boxes.SampleTableBox; +import com.coremedia.iso.boxes.SampleToChunkBox; +import com.coremedia.iso.boxes.StaticChunkOffsetBox; +import com.coremedia.iso.boxes.SyncSampleBox; +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.coremedia.iso.boxes.TrackBox; +import com.coremedia.iso.boxes.TrackHeaderBox; +import com.googlecode.mp4parser.authoring.DateHelper; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Creates a plain MP4 file from a video. Plain as plain can be. + */ +public class DefaultMp4Builder implements Mp4Builder { + + public int STEPSIZE = 64; + Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>(); + private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName()); + + HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>(); + HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>(); + private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder(); + + public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { + this.intersectionFinder = intersectionFinder; + } + + /** + * {@inheritDoc} + */ + public IsoFile build(Movie movie) { + LOG.fine("Creating movie " + movie); + for (Track track : movie.getTracks()) { + // getting the samples may be a time consuming activity + List<ByteBuffer> samples = track.getSamples(); + putSamples(track, samples); + long[] sizes = new long[samples.size()]; + for (int i = 0; i < sizes.length; i++) { + sizes[i] = samples.get(i).limit(); + } + putSampleSizes(track, sizes); + } + + IsoFile isoFile = new IsoFile(); + // ouch that is ugly but I don't know how to do it else + List<String> minorBrands = new LinkedList<String>(); + minorBrands.add("isom"); + minorBrands.add("iso2"); + minorBrands.add("avc1"); + + isoFile.addBox(new FileTypeBox("isom", 0, minorBrands)); + isoFile.addBox(createMovieBox(movie)); + InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie); + isoFile.addBox(mdat); + + /* + dataOffset is where the first sample starts. In this special mdat the samples always start + at offset 16 so that we can use the same offset for large boxes and small boxes + */ + long dataOffset = mdat.getDataOffset(); + for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) { + long[] offsets = chunkOffsetBox.getChunkOffsets(); + for (int i = 0; i < offsets.length; i++) { + offsets[i] += dataOffset; + } + } + + + return isoFile; + } + + public FragmentIntersectionFinder getFragmentIntersectionFinder() { + throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!"); + } + + protected long[] putSampleSizes(Track track, long[] sizes) { + return track2SampleSizes.put(track, sizes); + } + + protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) { + return track2Sample.put(track, samples); + } + + private MovieBox createMovieBox(Movie movie) { + MovieBox movieBox = new MovieBox(); + MovieHeaderBox mvhd = new MovieHeaderBox(); + + mvhd.setCreationTime(DateHelper.convert(new Date())); + mvhd.setModificationTime(DateHelper.convert(new Date())); + + long movieTimeScale = getTimescale(movie); + long duration = 0; + + for (Track track : movie.getTracks()) { + long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); + if (tracksDuration > duration) { + duration = tracksDuration; + } + + + } + + mvhd.setDuration(duration); + mvhd.setTimescale(movieTimeScale); + // find the next available trackId + long nextTrackId = 0; + for (Track track : movie.getTracks()) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + mvhd.setNextTrackId(++nextTrackId); + if (mvhd.getCreationTime() >= 1l << 32 || + mvhd.getModificationTime() >= 1l << 32 || + mvhd.getDuration() >= 1l << 32) { + mvhd.setVersion(1); + } + + movieBox.addBox(mvhd); + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrackBox(track, movie)); + } + // metadata here + Box udta = createUdta(movie); + if (udta != null) { + movieBox.addBox(udta); + } + return movieBox; + + } + + /** + * Override to create a user data box that may contain metadata. + * + * @return a 'udta' box or <code>null</code> if none provided + */ + protected Box createUdta(Movie movie) { + return null; + } + + private TrackBox createTrackBox(Track track, Movie movie) { + + LOG.info("Creating Mp4TrackImpl " + track); + TrackBox trackBox = new TrackBox(); + TrackHeaderBox tkhd = new TrackHeaderBox(); + int flags = 0; + if (track.isEnabled()) { + flags += 1; + } + + if (track.isInMovie()) { + flags += 2; + } + + if (track.isInPreview()) { + flags += 4; + } + + if (track.isInPoster()) { + flags += 8; + } + tkhd.setFlags(flags); + + tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); + tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + // We need to take edit list box into account in trackheader duration + // but as long as I don't support edit list boxes it is sufficient to + // just translate media duration to movie timescale + tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale()); + tkhd.setHeight(track.getTrackMetaData().getHeight()); + tkhd.setWidth(track.getTrackMetaData().getWidth()); + tkhd.setLayer(track.getTrackMetaData().getLayer()); + tkhd.setModificationTime(DateHelper.convert(new Date())); + tkhd.setTrackId(track.getTrackMetaData().getTrackId()); + tkhd.setVolume(track.getTrackMetaData().getVolume()); + if (tkhd.getCreationTime() >= 1l << 32 || + tkhd.getModificationTime() >= 1l << 32 || + tkhd.getDuration() >= 1l << 32) { + tkhd.setVersion(1); + } + + trackBox.addBox(tkhd); + +/* + EditBox edit = new EditBox(); + EditListBox editListBox = new EditListBox(); + editListBox.setEntries(Collections.singletonList( + new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1))); + edit.addBox(editListBox); + trackBox.addBox(edit); +*/ + + MediaBox mdia = new MediaBox(); + trackBox.addBox(mdia); + MediaHeaderBox mdhd = new MediaHeaderBox(); + mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + mdhd.setDuration(getDuration(track)); + mdhd.setTimescale(track.getTrackMetaData().getTimescale()); + mdhd.setLanguage(track.getTrackMetaData().getLanguage()); + mdia.addBox(mdhd); + HandlerBox hdlr = new HandlerBox(); + mdia.addBox(hdlr); + + hdlr.setHandlerType(track.getHandler()); + + MediaInformationBox minf = new MediaInformationBox(); + minf.addBox(track.getMediaHeaderBox()); + + // dinf: all these three boxes tell us is that the actual + // data is in the current file and not somewhere external + DataInformationBox dinf = new DataInformationBox(); + DataReferenceBox dref = new DataReferenceBox(); + dinf.addBox(dref); + DataEntryUrlBox url = new DataEntryUrlBox(); + url.setFlags(1); + dref.addBox(url); + minf.addBox(dinf); + // + + SampleTableBox stbl = new SampleTableBox(); + + stbl.addBox(track.getSampleDescriptionBox()); + + List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries(); + if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) { + TimeToSampleBox stts = new TimeToSampleBox(); + stts.setEntries(track.getDecodingTimeEntries()); + stbl.addBox(stts); + } + + List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries(); + if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) { + CompositionTimeToSample ctts = new CompositionTimeToSample(); + ctts.setEntries(compositionTimeToSampleEntries); + stbl.addBox(ctts); + } + + long[] syncSamples = track.getSyncSamples(); + if (syncSamples != null && syncSamples.length > 0) { + SyncSampleBox stss = new SyncSampleBox(); + stss.setSampleNumber(syncSamples); + stbl.addBox(stss); + } + + if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { + SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox(); + sdtp.setEntries(track.getSampleDependencies()); + stbl.addBox(sdtp); + } + HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>(); + for (Track current : movie.getTracks()) { + track2ChunkSizes.put(current, getChunkSizes(current, movie)); + } + int[] tracksChunkSizes = track2ChunkSizes.get(track); + + SampleToChunkBox stsc = new SampleToChunkBox(); + stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>()); + long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size + for (int i = 0; i < tracksChunkSizes.length; i++) { + // The sample description index references the sample description box + // that describes the samples of this chunk. My Tracks cannot have more + // than one sample description box. Therefore 1 is always right + // the first chunk has the number '1' + if (lastChunkSize != tracksChunkSizes[i]) { + stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1)); + lastChunkSize = tracksChunkSizes[i]; + } + } + stbl.addBox(stsc); + + SampleSizeBox stsz = new SampleSizeBox(); + stsz.setSampleSizes(track2SampleSizes.get(track)); + + stbl.addBox(stsz); + // The ChunkOffsetBox we create here is just a stub + // since we haven't created the whole structure we can't tell where the + // first chunk starts (mdat box). So I just let the chunk offset + // start at zero and I will add the mdat offset later. + StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); + this.chunkOffsetBoxes.add(stco); + long offset = 0; + long[] chunkOffset = new long[tracksChunkSizes.length]; + // all tracks have the same number of chunks + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId()); + } + + + for (int i = 0; i < tracksChunkSizes.length; i++) { + // The filelayout will be: + // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n + // calculating the offsets + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i); + } + for (Track current : movie.getTracks()) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId()); + } + int[] chunkSizes = track2ChunkSizes.get(current); + long firstSampleOfChunk = 0; + for (int j = 0; j < i; j++) { + firstSampleOfChunk += chunkSizes[j]; + } + if (current == track) { + chunkOffset[i] = offset; + } + for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { + offset += track2SampleSizes.get(current)[j]; + } + } + } + stco.setChunkOffsets(chunkOffset); + stbl.addBox(stco); + minf.addBox(stbl); + mdia.addBox(minf); + + return trackBox; + } + + private class InterleaveChunkMdat implements Box { + List<Track> tracks; + List<ByteBuffer> samples = new ArrayList<ByteBuffer>(); + ContainerBox parent; + + long contentSize = 0; + + public ContainerBox getParent() { + return parent; + } + + public void setParent(ContainerBox parent) { + this.parent = parent; + } + + public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { + } + + private InterleaveChunkMdat(Movie movie) { + + tracks = movie.getTracks(); + Map<Track, int[]> chunks = new HashMap<Track, int[]>(); + for (Track track : movie.getTracks()) { + chunks.put(track, getChunkSizes(track, movie)); + } + + for (int i = 0; i < chunks.values().iterator().next().length; i++) { + for (Track track : tracks) { + + int[] chunkSizes = chunks.get(track); + long firstSampleOfChunk = 0; + for (int j = 0; j < i; j++) { + firstSampleOfChunk += chunkSizes[j]; + } + + for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { + + ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j); + contentSize += s.limit(); + samples.add((ByteBuffer) s.rewind()); + } + + } + + } + + } + + public long getDataOffset() { + Box b = this; + long offset = 16; + while (b.getParent() != null) { + for (Box box : b.getParent().getBoxes()) { + if (b == box) { + break; + } + offset += box.getSize(); + } + b = b.getParent(); + } + return offset; + } + + + public String getType() { + return "mdat"; + } + + public long getSize() { + return 16 + contentSize; + } + + private boolean isSmallBox(long contentSize) { + return (contentSize + 8) < 4294967296L; + } + + + public void getBox(WritableByteChannel writableByteChannel) throws IOException { + ByteBuffer bb = ByteBuffer.allocate(16); + long size = getSize(); + if (isSmallBox(size)) { + IsoTypeWriter.writeUInt32(bb, size); + } else { + IsoTypeWriter.writeUInt32(bb, 1); + } + bb.put(IsoFile.fourCCtoBytes("mdat")); + if (isSmallBox(size)) { + bb.put(new byte[8]); + } else { + IsoTypeWriter.writeUInt64(bb, size); + } + bb.rewind(); + writableByteChannel.write(bb); + if (writableByteChannel instanceof GatheringByteChannel) { + List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples); + + + for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) { + List<ByteBuffer> sublist = nuSamples.subList( + i * STEPSIZE, // start + (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end + ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); + do { + ((GatheringByteChannel) writableByteChannel).write(sampleArray); + } while (sampleArray[sampleArray.length - 1].remaining() > 0); + } + //System.err.println(bytesWritten); + } else { + for (ByteBuffer sample : samples) { + sample.rewind(); + writableByteChannel.write(sample); + } + } + } + + } + + /** + * Gets the chunk sizes for the given track. + * + * @param track + * @param movie + * @return + */ + int[] getChunkSizes(Track track, Movie movie) { + + long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie); + int[] chunkSizes = new int[referenceChunkStarts.length]; + + + for (int i = 0; i < referenceChunkStarts.length; i++) { + long start = referenceChunkStarts[i] - 1; + long end; + if (referenceChunkStarts.length == i + 1) { + end = track.getSamples().size(); + } else { + end = referenceChunkStarts[i + 1] - 1; + } + + chunkSizes[i] = l2i(end - start); + // The Stretch makes sure that there are as much audio and video chunks! + } + assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal"; + return chunkSizes; + + + } + + + private static long sum(int[] ls) { + long rc = 0; + for (long l : ls) { + rc += l; + } + return rc; + } + + protected static long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + public long getTimescale(Movie movie) { + long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale(); + for (Track track : movie.getTracks()) { + timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); + } + return timescale; + } + + public static long gcd(long a, long b) { + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + + public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) { + ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); + for (ByteBuffer buffer : samples) { + int lastIndex = nuSamples.size() - 1; + if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && + nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { + ByteBuffer oldBuffer = nuSamples.remove(lastIndex); + ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); + // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. + nuSamples.add(nu); + } else if (lastIndex >= 0 && + buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && + nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { + // This can go wrong - but will it? + ByteBuffer oldBuffer = nuSamples.get(lastIndex); + oldBuffer.limit(buffer.limit() + oldBuffer.limit()); + } else { + nuSamples.add(buffer); + } + } + return nuSamples; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentIntersectionFinder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentIntersectionFinder.java.svn-base new file mode 100644 index 0000000..1224bbf --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentIntersectionFinder.java.svn-base @@ -0,0 +1,34 @@ +/* + * 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.builder; + +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +/** + * + */ +public interface FragmentIntersectionFinder { + /** + * Gets the ordinal number of the samples which will be the first sample + * in each fragment. + * + * @param track concerned track + * @param movie the context of the track + * @return an array containing the ordinal of each fragment's first sample + */ + public long[] sampleNumbers(Track track, Movie movie); +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentedMp4Builder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentedMp4Builder.java.svn-base new file mode 100644 index 0000000..c65ff1c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/FragmentedMp4Builder.java.svn-base @@ -0,0 +1,742 @@ +/* + * 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.builder; + +import com.coremedia.iso.BoxParser; +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.IsoTypeWriter; +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.fragment.*; +import com.googlecode.mp4parser.authoring.DateHelper; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.*; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Creates a fragmented MP4 file. + */ +public class FragmentedMp4Builder implements Mp4Builder { + private static final Logger LOG = Logger.getLogger(FragmentedMp4Builder.class.getName()); + + protected FragmentIntersectionFinder intersectionFinder; + + public FragmentedMp4Builder() { + this.intersectionFinder = new SyncSampleIntersectFinderImpl(); + } + + public List<String> getAllowedHandlers() { + return Arrays.asList("soun", "vide"); + } + + public Box createFtyp(Movie movie) { + List<String> minorBrands = new LinkedList<String>(); + minorBrands.add("isom"); + minorBrands.add("iso2"); + minorBrands.add("avc1"); + return new FileTypeBox("isom", 0, minorBrands); + } + + /** + * Some formats require sorting of the fragments. E.g. Ultraviolet CFF files are required + * to contain the fragments size sort: + * <ul> + * <li>video[1].getBytes().length < audio[1].getBytes().length < subs[1].getBytes().length</li> + * <li> audio[2].getBytes().length < video[2].getBytes().length < subs[2].getBytes().length</li> + * </ul> + * + * make this fragment: + * + * <ol> + * <li>video[1]</li> + * <li>audio[1]</li> + * <li>subs[1]</li> + * <li>audio[2]</li> + * <li>video[2]</li> + * <li>subs[2]</li> + * </ol> + * + * @param tracks the list of tracks to returned sorted + * @param cycle current fragment (sorting may vary between the fragments) + * @param intersectionMap a map from tracks to their fragments' first samples. + * @return the list of tracks in order of appearance in the fragment + */ + protected List<Track> sortTracksInSequence(List<Track> tracks, final int cycle, final Map<Track, long[]> intersectionMap) { + tracks = new LinkedList<Track>(tracks); + Collections.sort(tracks, new Comparator<Track>() { + public int compare(Track o1, Track o2) { + long[] startSamples1 = intersectionMap.get(o1); + long startSample1 = startSamples1[cycle]; + // one based sample numbers - the first sample is 1 + long endSample1 = cycle + 1 < startSamples1.length ? startSamples1[cycle + 1] : o1.getSamples().size() + 1; + long[] startSamples2 = intersectionMap.get(o2); + long startSample2 = startSamples2[cycle]; + // one based sample numbers - the first sample is 1 + long endSample2 = cycle + 1 < startSamples2.length ? startSamples2[cycle + 1] : o2.getSamples().size() + 1; + List<ByteBuffer> samples1 = o1.getSamples().subList(l2i(startSample1) - 1, l2i(endSample1) - 1); + List<ByteBuffer> samples2 = o2.getSamples().subList(l2i(startSample2) - 1, l2i(endSample2) - 1); + int size1 = 0; + for (ByteBuffer byteBuffer : samples1) { + size1 += byteBuffer.limit(); + } + int size2 = 0; + for (ByteBuffer byteBuffer : samples2) { + size2 += byteBuffer.limit(); + } + return size1 - size2; + } + }); + return tracks; + } + + protected List<Box> createMoofMdat(final Movie movie) { + List<Box> boxes = new LinkedList<Box>(); + HashMap<Track, long[]> intersectionMap = new HashMap<Track, long[]>(); + int maxNumberOfFragments = 0; + for (Track track : movie.getTracks()) { + long[] intersects = intersectionFinder.sampleNumbers(track, movie); + intersectionMap.put(track, intersects); + maxNumberOfFragments = Math.max(maxNumberOfFragments, intersects.length); + } + + + int sequence = 1; + // this loop has two indices: + + for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) { + + final List<Track> sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap); + + for (Track track : sortedTracks) { + if (getAllowedHandlers().isEmpty() || getAllowedHandlers().contains(track.getHandler())) { + long[] startSamples = intersectionMap.get(track); + //some tracks may have less fragments -> skip them + if (cycle < startSamples.length) { + + long startSample = startSamples[cycle]; + // one based sample numbers - the first sample is 1 + long endSample = cycle + 1 < startSamples.length ? startSamples[cycle + 1] : track.getSamples().size() + 1; + + // if startSample == endSample the cycle is empty! + if (startSample != endSample) { + boxes.add(createMoof(startSample, endSample, track, sequence)); + boxes.add(createMdat(startSample, endSample, track, sequence++)); + } + } + } + } + } + return boxes; + } + + /** + * {@inheritDoc} + */ + public IsoFile build(Movie movie) { + LOG.fine("Creating movie " + movie); + IsoFile isoFile = new IsoFile(); + + + isoFile.addBox(createFtyp(movie)); + isoFile.addBox(createMoov(movie)); + + for (Box box : createMoofMdat(movie)) { + isoFile.addBox(box); + } + isoFile.addBox(createMfra(movie, isoFile)); + + return isoFile; + } + + protected Box createMdat(final long startSample, final long endSample, final Track track, final int i) { + + class Mdat implements Box { + ContainerBox parent; + + public ContainerBox getParent() { + return parent; + } + + public void setParent(ContainerBox parent) { + this.parent = parent; + } + + public long getSize() { + long size = 8; // I don't expect 2gig fragments + for (ByteBuffer sample : getSamples(startSample, endSample, track, i)) { + size += sample.limit(); + } + return size; + } + + public String getType() { + return "mdat"; + } + + public void getBox(WritableByteChannel writableByteChannel) throws IOException { + List<ByteBuffer> bbs = getSamples(startSample, endSample, track, i); + final List<ByteBuffer> samples = ByteBufferHelper.mergeAdjacentBuffers(bbs); + ByteBuffer header = ByteBuffer.allocate(8); + IsoTypeWriter.writeUInt32(header, l2i(getSize())); + header.put(IsoFile.fourCCtoBytes(getType())); + header.rewind(); + writableByteChannel.write(header); + if (writableByteChannel instanceof GatheringByteChannel) { + + int STEPSIZE = 1024; + // This is required to prevent android from crashing + // it seems that {@link GatheringByteChannel#write(java.nio.ByteBuffer[])} + // just handles up to 1024 buffers + for (int i = 0; i < Math.ceil((double) samples.size() / STEPSIZE); i++) { + List<ByteBuffer> sublist = samples.subList( + i * STEPSIZE, // start + (i + 1) * STEPSIZE < samples.size() ? (i + 1) * STEPSIZE : samples.size()); // end + ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); + do { + ((GatheringByteChannel) writableByteChannel).write(sampleArray); + } while (sampleArray[sampleArray.length - 1].remaining() > 0); + } + //System.err.println(bytesWritten); + } else { + for (ByteBuffer sample : samples) { + sample.rewind(); + writableByteChannel.write(sample); + } + } + + } + + public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { + + } + } + + return new Mdat(); + } + + protected Box createTfhd(long startSample, long endSample, Track track, int sequenceNumber) { + TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox(); + SampleFlags sf = new SampleFlags(); + + tfhd.setDefaultSampleFlags(sf); + tfhd.setBaseDataOffset(-1); + tfhd.setTrackId(track.getTrackMetaData().getTrackId()); + return tfhd; + } + + protected Box createMfhd(long startSample, long endSample, Track track, int sequenceNumber) { + MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox(); + mfhd.setSequenceNumber(sequenceNumber); + return mfhd; + } + + protected Box createTraf(long startSample, long endSample, Track track, int sequenceNumber) { + TrackFragmentBox traf = new TrackFragmentBox(); + traf.addBox(createTfhd(startSample, endSample, track, sequenceNumber)); + for (Box trun : createTruns(startSample, endSample, track, sequenceNumber)) { + traf.addBox(trun); + } + + return traf; + } + + + /** + * Gets the all samples starting with <code>startSample</code> (one based -> one is the first) and + * ending with <code>endSample</code> (exclusive). + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return a <code>List<ByteBuffer></code> of raw samples + */ + protected List<ByteBuffer> getSamples(long startSample, long endSample, Track track, int sequenceNumber) { + // since startSample and endSample are one-based substract 1 before addressing list elements + return track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); + } + + /** + * Gets the sizes of a sequence of samples- + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return + */ + protected long[] getSampleSizes(long startSample, long endSample, Track track, int sequenceNumber) { + List<ByteBuffer> samples = getSamples(startSample, endSample, track, sequenceNumber); + + long[] sampleSizes = new long[samples.size()]; + for (int i = 0; i < sampleSizes.length; i++) { + sampleSizes[i] = samples.get(i).limit(); + } + return sampleSizes; + } + + /** + * Creates one or more track run boxes for a given sequence. + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return the list of TrackRun boxes. + */ + protected List<? extends Box> createTruns(long startSample, long endSample, Track track, int sequenceNumber) { + TrackRunBox trun = new TrackRunBox(); + long[] sampleSizes = getSampleSizes(startSample, endSample, track, sequenceNumber); + + trun.setSampleDurationPresent(true); + trun.setSampleSizePresent(true); + List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(l2i(endSample - startSample)); + + + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + long left = startSample - 1; + long curEntryLeft = timeQueue.peek().getCount(); + while (left > curEntryLeft) { + left -= curEntryLeft; + timeQueue.remove(); + curEntryLeft = timeQueue.peek().getCount(); + } + curEntryLeft -= left; + + + Queue<CompositionTimeToSample.Entry> compositionTimeQueue = + track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0 ? + new LinkedList<CompositionTimeToSample.Entry>(track.getCompositionTimeEntries()) : null; + long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue.peek().getCount() : -1; + + + trun.setSampleCompositionTimeOffsetPresent(compositionTimeEntriesLeft > 0); + + // fast forward composition stuff + for (long i = 1; i < startSample; i++) { + if (compositionTimeQueue != null) { + //trun.setSampleCompositionTimeOffsetPresent(true); + if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { + compositionTimeQueue.remove(); + compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); + } + } + } + + boolean sampleFlagsRequired = (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() || + track.getSyncSamples() != null && track.getSyncSamples().length != 0); + + trun.setSampleFlagsPresent(sampleFlagsRequired); + + for (int i = 0; i < sampleSizes.length; i++) { + TrackRunBox.Entry entry = new TrackRunBox.Entry(); + entry.setSampleSize(sampleSizes[i]); + if (sampleFlagsRequired) { + //if (false) { + SampleFlags sflags = new SampleFlags(); + + if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { + SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i); + sflags.setSampleDependsOn(e.getSampleDependsOn()); + sflags.setSampleIsDependedOn(e.getSampleIsDependentOn()); + sflags.setSampleHasRedundancy(e.getSampleHasRedundancy()); + } + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + // we have to mark non-sync samples! + if (Arrays.binarySearch(track.getSyncSamples(), startSample + i) >= 0) { + sflags.setSampleIsDifferenceSample(false); + sflags.setSampleDependsOn(2); + } else { + sflags.setSampleIsDifferenceSample(true); + sflags.setSampleDependsOn(1); + } + } + // i don't have sample degradation + entry.setSampleFlags(sflags); + + } + + entry.setSampleDuration(timeQueue.peek().getDelta()); + if (--curEntryLeft == 0 && timeQueue.size() > 1) { + timeQueue.remove(); + curEntryLeft = timeQueue.peek().getCount(); + } + + if (compositionTimeQueue != null) { + entry.setSampleCompositionTimeOffset(compositionTimeQueue.peek().getOffset()); + if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { + compositionTimeQueue.remove(); + compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); + } + } + entries.add(entry); + } + + trun.setEntries(entries); + + return Collections.singletonList(trun); + } + + /** + * Creates a 'moof' box for a given sequence of samples. + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return the list of TrackRun boxes. + */ + protected Box createMoof(long startSample, long endSample, Track track, int sequenceNumber) { + MovieFragmentBox moof = new MovieFragmentBox(); + moof.addBox(createMfhd(startSample, endSample, track, sequenceNumber)); + moof.addBox(createTraf(startSample, endSample, track, sequenceNumber)); + + TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0); + firstTrun.setDataOffset(1); // dummy to make size correct + firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size + + return moof; + } + + /** + * Creates a single 'mvhd' movie header box for a given movie. + * + * @param movie the concerned movie + * @return an 'mvhd' box + */ + protected Box createMvhd(Movie movie) { + MovieHeaderBox mvhd = new MovieHeaderBox(); + mvhd.setVersion(1); + mvhd.setCreationTime(DateHelper.convert(new Date())); + mvhd.setModificationTime(DateHelper.convert(new Date())); + long movieTimeScale = movie.getTimescale(); + long duration = 0; + + for (Track track : movie.getTracks()) { + long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); + if (tracksDuration > duration) { + duration = tracksDuration; + } + + + } + + mvhd.setDuration(duration); + mvhd.setTimescale(movieTimeScale); + // find the next available trackId + long nextTrackId = 0; + for (Track track : movie.getTracks()) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + mvhd.setNextTrackId(++nextTrackId); + return mvhd; + } + + /** + * Creates a fully populated 'moov' box with all child boxes. Child boxes are: + * <ul> + * <li>{@link #createMvhd(com.googlecode.mp4parser.authoring.Movie) mvhd}</li> + * <li>{@link #createMvex(com.googlecode.mp4parser.authoring.Movie) mvex}</li> + * <li>a {@link #createTrak(com.googlecode.mp4parser.authoring.Track, com.googlecode.mp4parser.authoring.Movie) trak} for every track</li> + * </ul> + * + * @param movie the concerned movie + * @return fully populated 'moov' + */ + protected Box createMoov(Movie movie) { + MovieBox movieBox = new MovieBox(); + + movieBox.addBox(createMvhd(movie)); + movieBox.addBox(createMvex(movie)); + + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrak(track, movie)); + } + // metadata here + return movieBox; + + } + + /** + * Creates a 'tfra' - track fragment random access box for the given track with the isoFile. + * The tfra contains a map of random access points with time as key and offset within the isofile + * as value. + * + * @param track the concerned track + * @param isoFile the track is contained in + * @return a track fragment random access box. + */ + protected Box createTfra(Track track, IsoFile isoFile) { + TrackFragmentRandomAccessBox tfra = new TrackFragmentRandomAccessBox(); + tfra.setVersion(1); // use long offsets and times + List<TrackFragmentRandomAccessBox.Entry> offset2timeEntries = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); + List<Box> boxes = isoFile.getBoxes(); + long offset = 0; + long duration = 0; + for (Box box : boxes) { + if (box instanceof MovieFragmentBox) { + List<TrackFragmentBox> trafs = ((MovieFragmentBox) box).getBoxes(TrackFragmentBox.class); + for (int i = 0; i < trafs.size(); i++) { + TrackFragmentBox traf = trafs.get(i); + if (traf.getTrackFragmentHeaderBox().getTrackId() == track.getTrackMetaData().getTrackId()) { + // here we are at the offset required for the current entry. + List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); + for (int j = 0; j < truns.size(); j++) { + List<TrackFragmentRandomAccessBox.Entry> offset2timeEntriesThisTrun = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); + TrackRunBox trun = truns.get(j); + for (int k = 0; k < trun.getEntries().size(); k++) { + TrackRunBox.Entry trunEntry = trun.getEntries().get(k); + SampleFlags sf = null; + if (k == 0 && trun.isFirstSampleFlagsPresent()) { + sf = trun.getFirstSampleFlags(); + } else if (trun.isSampleFlagsPresent()) { + sf = trunEntry.getSampleFlags(); + } else { + List<MovieExtendsBox> mvexs = isoFile.getMovieBox().getBoxes(MovieExtendsBox.class); + for (MovieExtendsBox mvex : mvexs) { + List<TrackExtendsBox> trexs = mvex.getBoxes(TrackExtendsBox.class); + for (TrackExtendsBox trex : trexs) { + if (trex.getTrackId() == track.getTrackMetaData().getTrackId()) { + sf = trex.getDefaultSampleFlags(); + } + } + } + + } + if (sf == null) { + throw new RuntimeException("Could not find any SampleFlags to indicate random access or not"); + } + if (sf.getSampleDependsOn() == 2) { + offset2timeEntriesThisTrun.add(new TrackFragmentRandomAccessBox.Entry( + duration, + offset, + i + 1, j + 1, k + 1)); + } + duration += trunEntry.getSampleDuration(); + } + if (offset2timeEntriesThisTrun.size() == trun.getEntries().size() && trun.getEntries().size() > 0) { + // Oooops every sample seems to be random access sample + // is this an audio track? I don't care. + // I just use the first for trun sample for tfra random access + offset2timeEntries.add(offset2timeEntriesThisTrun.get(0)); + } else { + offset2timeEntries.addAll(offset2timeEntriesThisTrun); + } + } + } + } + } + + + offset += box.getSize(); + } + tfra.setEntries(offset2timeEntries); + tfra.setTrackId(track.getTrackMetaData().getTrackId()); + return tfra; + } + + /** + * Creates a 'mfra' - movie fragment random access box for the given movie in the given + * isofile. Uses {@link #createTfra(com.googlecode.mp4parser.authoring.Track, com.coremedia.iso.IsoFile)} + * to generate the child boxes. + * + * @param movie concerned movie + * @param isoFile concerned isofile + * @return a complete 'mfra' box + */ + protected Box createMfra(Movie movie, IsoFile isoFile) { + MovieFragmentRandomAccessBox mfra = new MovieFragmentRandomAccessBox(); + for (Track track : movie.getTracks()) { + mfra.addBox(createTfra(track, isoFile)); + } + + MovieFragmentRandomAccessOffsetBox mfro = new MovieFragmentRandomAccessOffsetBox(); + mfra.addBox(mfro); + mfro.setMfraSize(mfra.getSize()); + return mfra; + } + + protected Box createTrex(Movie movie, Track track) { + TrackExtendsBox trex = new TrackExtendsBox(); + trex.setTrackId(track.getTrackMetaData().getTrackId()); + trex.setDefaultSampleDescriptionIndex(1); + trex.setDefaultSampleDuration(0); + trex.setDefaultSampleSize(0); + SampleFlags sf = new SampleFlags(); + if ("soun".equals(track.getHandler())) { + // as far as I know there is no audio encoding + // where the sample are not self contained. + sf.setSampleDependsOn(2); + sf.setSampleIsDependedOn(2); + } + trex.setDefaultSampleFlags(sf); + return trex; + } + + /** + * Creates a 'mvex' - movie extends box and populates it with 'trex' boxes + * by calling {@link #createTrex(com.googlecode.mp4parser.authoring.Movie, com.googlecode.mp4parser.authoring.Track)} + * for each track to generate them + * + * @param movie the source movie + * @return a complete 'mvex' + */ + protected Box createMvex(Movie movie) { + MovieExtendsBox mvex = new MovieExtendsBox(); + final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox(); + for (Track track : movie.getTracks()) { + final long trackDuration = getTrackDuration(movie, track); + if (mved.getFragmentDuration() < trackDuration) { + mved.setFragmentDuration(trackDuration); + } + } + mvex.addBox(mved); + + for (Track track : movie.getTracks()) { + mvex.addBox(createTrex(movie, track)); + } + return mvex; + } + + protected Box createTkhd(Movie movie, Track track) { + TrackHeaderBox tkhd = new TrackHeaderBox(); + tkhd.setVersion(1); + int flags = 0; + if (track.isEnabled()) { + flags += 1; + } + + if (track.isInMovie()) { + flags += 2; + } + + if (track.isInPreview()) { + flags += 4; + } + + if (track.isInPoster()) { + flags += 8; + } + tkhd.setFlags(flags); + + tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); + tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + // We need to take edit list box into account in trackheader duration + // but as long as I don't support edit list boxes it is sufficient to + // just translate media duration to movie timescale + tkhd.setDuration(getTrackDuration(movie, track)); + tkhd.setHeight(track.getTrackMetaData().getHeight()); + tkhd.setWidth(track.getTrackMetaData().getWidth()); + tkhd.setLayer(track.getTrackMetaData().getLayer()); + tkhd.setModificationTime(DateHelper.convert(new Date())); + tkhd.setTrackId(track.getTrackMetaData().getTrackId()); + tkhd.setVolume(track.getTrackMetaData().getVolume()); + return tkhd; + } + + private long getTrackDuration(Movie movie, Track track) { + return getDuration(track) * movie.getTimescale() / track.getTrackMetaData().getTimescale(); + } + + protected Box createMdhd(Movie movie, Track track) { + MediaHeaderBox mdhd = new MediaHeaderBox(); + mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + mdhd.setDuration(getDuration(track)); + mdhd.setTimescale(track.getTrackMetaData().getTimescale()); + mdhd.setLanguage(track.getTrackMetaData().getLanguage()); + return mdhd; + } + + protected Box createStbl(Movie movie, Track track) { + SampleTableBox stbl = new SampleTableBox(); + + stbl.addBox(track.getSampleDescriptionBox()); + stbl.addBox(new TimeToSampleBox()); + //stbl.addBox(new SampleToChunkBox()); + stbl.addBox(new StaticChunkOffsetBox()); + return stbl; + } + + protected Box createMinf(Track track, Movie movie) { + MediaInformationBox minf = new MediaInformationBox(); + minf.addBox(track.getMediaHeaderBox()); + minf.addBox(createDinf(movie, track)); + minf.addBox(createStbl(movie, track)); + return minf; + } + + protected Box createMdiaHdlr(Track track, Movie movie) { + HandlerBox hdlr = new HandlerBox(); + hdlr.setHandlerType(track.getHandler()); + return hdlr; + } + + protected Box createMdia(Track track, Movie movie) { + MediaBox mdia = new MediaBox(); + mdia.addBox(createMdhd(movie, track)); + + + mdia.addBox(createMdiaHdlr(track, movie)); + + + mdia.addBox(createMinf(track, movie)); + return mdia; + } + + protected Box createTrak(Track track, Movie movie) { + LOG.fine("Creating Track " + track); + TrackBox trackBox = new TrackBox(); + trackBox.addBox(createTkhd(movie, track)); + trackBox.addBox(createMdia(track, movie)); + return trackBox; + } + + protected DataInformationBox createDinf(Movie movie, Track track) { + DataInformationBox dinf = new DataInformationBox(); + DataReferenceBox dref = new DataReferenceBox(); + dinf.addBox(dref); + DataEntryUrlBox url = new DataEntryUrlBox(); + url.setFlags(1); + dref.addBox(url); + return dinf; + } + + public FragmentIntersectionFinder getFragmentIntersectionFinder() { + return intersectionFinder; + } + + public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { + this.intersectionFinder = intersectionFinder; + } + + protected long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/Mp4Builder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/Mp4Builder.java.svn-base new file mode 100644 index 0000000..725745e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/Mp4Builder.java.svn-base @@ -0,0 +1,35 @@ +/* + * 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.builder; + +import com.coremedia.iso.IsoFile; +import com.googlecode.mp4parser.authoring.Movie; + +/** + * Transforms a <code>Movie</code> object to an IsoFile. Implementations can + * determine the specific format: Fragmented MP4, MP4, MP4 with Apple Metadata, + * MP4 with 3GPP Metadata, MOV. + */ +public interface Mp4Builder { + /** + * Builds the actual IsoFile from the Movie. + * + * @param movie data source + * @return the freshly built IsoFile + */ + public IsoFile build(Movie movie); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/SyncSampleIntersectFinderImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/SyncSampleIntersectFinderImpl.java.svn-base new file mode 100644 index 0000000..2766c5e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/SyncSampleIntersectFinderImpl.java.svn-base @@ -0,0 +1,334 @@ +/* + * 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.builder; + +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.Math.lcm; + +/** + * This <code>FragmentIntersectionFinder</code> cuts the input movie video tracks in + * fragments of the same length exactly before the sync samples. Audio tracks are cut + * into pieces of similar length. + */ +public class SyncSampleIntersectFinderImpl implements FragmentIntersectionFinder { + + private static Logger LOG = Logger.getLogger(SyncSampleIntersectFinderImpl.class.getName()); + private static Map<CacheTuple, long[]> getTimesCache = new ConcurrentHashMap<CacheTuple, long[]>(); + private static Map<CacheTuple, long[]> getSampleNumbersCache = new ConcurrentHashMap<CacheTuple, long[]>(); + + private final int minFragmentDurationSeconds; + + public SyncSampleIntersectFinderImpl() { + minFragmentDurationSeconds = 0; + } + + /** + * Creates a <code>SyncSampleIntersectFinderImpl</code> that will not create any fragment + * smaller than the given <code>minFragmentDurationSeconds</code> + * + * @param minFragmentDurationSeconds the smallest allowable duration of a fragment. + */ + public SyncSampleIntersectFinderImpl(int minFragmentDurationSeconds) { + this.minFragmentDurationSeconds = minFragmentDurationSeconds; + } + + /** + * Gets an array of sample numbers that are meant to be the first sample of each + * chunk or fragment. + * + * @param track concerned track + * @param movie the context of the track + * @return an array containing the ordinal of each fragment's first sample + */ + public long[] sampleNumbers(Track track, Movie movie) { + final CacheTuple key = new CacheTuple(track, movie); + final long[] result = getSampleNumbersCache.get(key); + if (result != null) { + return result; + } + + if ("vide".equals(track.getHandler())) { + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + List<long[]> times = getSyncSamplesTimestamps(movie, track); + final long[] commonIndices = getCommonIndices(track.getSyncSamples(), getTimes(track, movie), track.getTrackMetaData().getTimescale(), times.toArray(new long[times.size()][])); + getSampleNumbersCache.put(key, commonIndices); + return commonIndices; + } else { + throw new RuntimeException("Video Tracks need sync samples. Only tracks other than video may have no sync samples."); + } + } else if ("soun".equals(track.getHandler())) { + Track referenceTrack = null; + for (Track candidate : movie.getTracks()) { + if (candidate.getSyncSamples() != null && "vide".equals(candidate.getHandler()) && candidate.getSyncSamples().length > 0) { + referenceTrack = candidate; + } + } + if (referenceTrack != null) { + + // Gets the reference track's fra + long[] refSyncSamples = sampleNumbers(referenceTrack, movie); + + int refSampleCount = referenceTrack.getSamples().size(); + + long[] syncSamples = new long[refSyncSamples.length]; + long minSampleRate = 192000; + for (Track testTrack : movie.getTracks()) { + if ("soun".equals(testTrack.getHandler())) { + AudioSampleEntry ase = (AudioSampleEntry) testTrack.getSampleDescriptionBox().getSampleEntry(); + if (ase.getSampleRate() < minSampleRate) { + minSampleRate = ase.getSampleRate(); + long sc = testTrack.getSamples().size(); + double stretch = (double) sc / refSampleCount; + TimeToSampleBox.Entry sttsEntry = testTrack.getDecodingTimeEntries().get(0); + long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types + + for (int i = 0; i < syncSamples.length; i++) { + long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1) * samplesPerFrame); + syncSamples[i] = start; + // The Stretch makes sure that there are as much audio and video chunks! + } + break; + } + } + } + AudioSampleEntry ase = (AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry(); + TimeToSampleBox.Entry sttsEntry = track.getDecodingTimeEntries().get(0); + long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types + double factor = (double) ase.getSampleRate() / (double) minSampleRate; + if (factor != Math.rint(factor)) { // Not an integer + throw new RuntimeException("Sample rates must be a multiple of the lowest sample rate to create a correct file!"); + } + for (int i = 0; i < syncSamples.length; i++) { + syncSamples[i] = (long) (1 + syncSamples[i] * factor / (double) samplesPerFrame); + } + getSampleNumbersCache.put(key, syncSamples); + return syncSamples; + } + throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!"); + } else { + // Ok, my track has no sync samples - let's find one with sync samples. + for (Track candidate : movie.getTracks()) { + if (candidate.getSyncSamples() != null && candidate.getSyncSamples().length > 0) { + long[] refSyncSamples = sampleNumbers(candidate, movie); + int refSampleCount = candidate.getSamples().size(); + + long[] syncSamples = new long[refSyncSamples.length]; + long sc = track.getSamples().size(); + double stretch = (double) sc / refSampleCount; + + for (int i = 0; i < syncSamples.length; i++) { + long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1)) + 1; + syncSamples[i] = start; + // The Stretch makes sure that there are as much audio and video chunks! + } + getSampleNumbersCache.put(key, syncSamples); + return syncSamples; + } + } + throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!"); + } + + + } + + /** + * Calculates the timestamp of all tracks' sync samples. + * + * @param movie + * @param track + * @return + */ + public static List<long[]> getSyncSamplesTimestamps(Movie movie, Track track) { + List<long[]> times = new LinkedList<long[]>(); + for (Track currentTrack : movie.getTracks()) { + if (currentTrack.getHandler().equals(track.getHandler())) { + long[] currentTrackSyncSamples = currentTrack.getSyncSamples(); + if (currentTrackSyncSamples != null && currentTrackSyncSamples.length > 0) { + final long[] currentTrackTimes = getTimes(currentTrack, movie); + times.add(currentTrackTimes); + } + } + } + return times; + } + + public long[] getCommonIndices(long[] syncSamples, long[] syncSampleTimes, long timeScale, long[]... otherTracksTimes) { + List<Long> nuSyncSamples = new LinkedList<Long>(); + List<Long> nuSyncSampleTimes = new LinkedList<Long>(); + + + for (int i = 0; i < syncSampleTimes.length; i++) { + boolean foundInEveryRef = true; + for (long[] times : otherTracksTimes) { + foundInEveryRef &= (Arrays.binarySearch(times, syncSampleTimes[i]) >= 0); + } + + if (foundInEveryRef) { + // use sample only if found in every other track. + nuSyncSamples.add(syncSamples[i]); + nuSyncSampleTimes.add(syncSampleTimes[i]); + } + } + // We have two arrays now: + // nuSyncSamples: Contains all common sync samples + // nuSyncSampleTimes: Contains the times of all sync samples + + // Start: Warn user if samples are not matching! + if (nuSyncSamples.size() < (syncSamples.length * 0.25)) { + String log = ""; + log += String.format("%5d - Common: [", nuSyncSamples.size()); + for (long l : nuSyncSamples) { + log += (String.format("%10d,", l)); + } + log += ("]"); + LOG.warning(log); + log = ""; + + log += String.format("%5d - In : [", syncSamples.length); + for (long l : syncSamples) { + log += (String.format("%10d,", l)); + } + log += ("]"); + LOG.warning(log); + LOG.warning("There are less than 25% of common sync samples in the given track."); + throw new RuntimeException("There are less than 25% of common sync samples in the given track."); + } else if (nuSyncSamples.size() < (syncSamples.length * 0.5)) { + LOG.fine("There are less than 50% of common sync samples in the given track. This is implausible but I'm ok to continue."); + } else if (nuSyncSamples.size() < syncSamples.length) { + LOG.finest("Common SyncSample positions vs. this tracks SyncSample positions: " + nuSyncSamples.size() + " vs. " + syncSamples.length); + } + // End: Warn user if samples are not matching! + + + + + List<Long> finalSampleList = new LinkedList<Long>(); + + if (minFragmentDurationSeconds > 0) { + // if minFragmentDurationSeconds is greater 0 + // we need to throw away certain samples. + long lastSyncSampleTime = -1; + Iterator<Long> nuSyncSamplesIterator = nuSyncSamples.iterator(); + Iterator<Long> nuSyncSampleTimesIterator = nuSyncSampleTimes.iterator(); + while (nuSyncSamplesIterator.hasNext() && nuSyncSampleTimesIterator.hasNext()) { + long curSyncSample = nuSyncSamplesIterator.next(); + long curSyncSampleTime = nuSyncSampleTimesIterator.next(); + if (lastSyncSampleTime == -1 || (curSyncSampleTime - lastSyncSampleTime) / timeScale >= minFragmentDurationSeconds) { + finalSampleList.add(curSyncSample); + lastSyncSampleTime = curSyncSampleTime; + } + } + } else { + // the list of all samples is the final list of samples + // since minFragmentDurationSeconds ist not used. + finalSampleList = nuSyncSamples; + } + + + // transform the list to an array + long[] finalSampleArray = new long[finalSampleList.size()]; + for (int i = 0; i < finalSampleArray.length; i++) { + finalSampleArray[i] = finalSampleList.get(i); + } + return finalSampleArray; + + } + + + private static long[] getTimes(Track track, Movie m) { + final CacheTuple key = new CacheTuple(track, m); + final long[] result = getTimesCache.get(key); + if (result != null) { + return result; + } + + long[] syncSamples = track.getSyncSamples(); + long[] syncSampleTimes = new long[syncSamples.length]; + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + + int currentSample = 1; // first syncsample is 1 + long currentDuration = 0; + long currentDelta = 0; + int currentSyncSampleIndex = 0; + long left = 0; + + final long scalingFactor = calculateTracktimesScalingFactor(m, track); + + while (currentSample <= syncSamples[syncSamples.length - 1]) { + if (currentSample++ == syncSamples[currentSyncSampleIndex]) { + syncSampleTimes[currentSyncSampleIndex++] = currentDuration * scalingFactor; + } + if (left-- == 0) { + TimeToSampleBox.Entry entry = timeQueue.poll(); + left = entry.getCount() - 1; + currentDelta = entry.getDelta(); + } + currentDuration += currentDelta; + } + getTimesCache.put(key, syncSampleTimes); + return syncSampleTimes; + } + + private static long calculateTracktimesScalingFactor(Movie m, Track track) { + long timeScale = 1; + for (Track track1 : m.getTracks()) { + if (track1.getHandler().equals(track.getHandler())) { + if (track1.getTrackMetaData().getTimescale() != track.getTrackMetaData().getTimescale()) { + timeScale = lcm(timeScale, track1.getTrackMetaData().getTimescale()); + } + } + } + return timeScale; + } + + public static class CacheTuple { + Track track; + Movie movie; + + public CacheTuple(Track track, Movie movie) { + this.track = track; + this.movie = movie; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CacheTuple that = (CacheTuple) o; + + if (movie != null ? !movie.equals(that.movie) : that.movie != null) return false; + if (track != null ? !track.equals(that.track) : that.track != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = track != null ? track.hashCode() : 0; + result = 31 * result + (movie != null ? movie.hashCode() : 0); + return result; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/TwoSecondIntersectionFinder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/TwoSecondIntersectionFinder.java.svn-base new file mode 100644 index 0000000..88aa4ab --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/TwoSecondIntersectionFinder.java.svn-base @@ -0,0 +1,86 @@ +/* + * 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.builder; + +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.util.Arrays; +import java.util.List; + +/** + * This <code>FragmentIntersectionFinder</code> cuts the input movie in 2 second + * snippets. + */ +public class TwoSecondIntersectionFinder implements FragmentIntersectionFinder { + + protected long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + /** + * {@inheritDoc} + */ + public long[] sampleNumbers(Track track, Movie movie) { + List<TimeToSampleBox.Entry> entries = track.getDecodingTimeEntries(); + + double trackLength = 0; + for (Track thisTrack : movie.getTracks()) { + double thisTracksLength = getDuration(thisTrack) / thisTrack.getTrackMetaData().getTimescale(); + if (trackLength < thisTracksLength) { + trackLength = thisTracksLength; + } + } + + int fragmentCount = (int)Math.ceil(trackLength / 2) - 1; + if (fragmentCount < 1) { + fragmentCount = 1; + } + + long fragments[] = new long[fragmentCount]; + Arrays.fill(fragments, -1); + fragments[0] = 1; + + long time = 0; + int samples = 0; + for (TimeToSampleBox.Entry entry : entries) { + for (int i = 0; i < entry.getCount(); i++) { + int currentFragment = (int) (time / track.getTrackMetaData().getTimescale() / 2) + 1; + if (currentFragment >= fragments.length) { + break; + } + fragments[currentFragment] = samples++ + 1; + time += entry.getDelta(); + } + } + long last = samples + 1; + // fill all -1 ones. + for (int i = fragments.length - 1; i >= 0; i--) { + if (fragments[i] == -1) { + fragments[i] = last ; + } + last = fragments[i]; + } + return fragments; + + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/ByteBufferHelper.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/ByteBufferHelper.java new file mode 100644 index 0000000..ad21b11 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/ByteBufferHelper.java @@ -0,0 +1,50 @@ +/* + * 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.builder; + +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Used to merge adjacent byte buffers. + */ +public class ByteBufferHelper { + public static List<ByteBuffer> mergeAdjacentBuffers(List<ByteBuffer> samples) { + ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); + for (ByteBuffer buffer : samples) { + int lastIndex = nuSamples.size() - 1; + if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && + nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { + ByteBuffer oldBuffer = nuSamples.remove(lastIndex); + ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); + // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. + nuSamples.add(nu); + } else if (lastIndex >= 0 && + buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && + nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { + // This can go wrong - but will it? + ByteBuffer oldBuffer = nuSamples.get(lastIndex); + oldBuffer.limit(buffer.limit() + oldBuffer.limit()); + } else { + buffer.rewind(); + nuSamples.add(buffer); + } + } + return nuSamples; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java new file mode 100644 index 0000000..9bd1ca6 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java @@ -0,0 +1,576 @@ +/* + * 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.builder; + +import com.coremedia.iso.BoxParser; +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.IsoTypeWriter; +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.CompositionTimeToSample; +import com.coremedia.iso.boxes.ContainerBox; +import com.coremedia.iso.boxes.DataEntryUrlBox; +import com.coremedia.iso.boxes.DataInformationBox; +import com.coremedia.iso.boxes.DataReferenceBox; +import com.coremedia.iso.boxes.FileTypeBox; +import com.coremedia.iso.boxes.HandlerBox; +import com.coremedia.iso.boxes.MediaBox; +import com.coremedia.iso.boxes.MediaHeaderBox; +import com.coremedia.iso.boxes.MediaInformationBox; +import com.coremedia.iso.boxes.MovieBox; +import com.coremedia.iso.boxes.MovieHeaderBox; +import com.coremedia.iso.boxes.SampleDependencyTypeBox; +import com.coremedia.iso.boxes.SampleSizeBox; +import com.coremedia.iso.boxes.SampleTableBox; +import com.coremedia.iso.boxes.SampleToChunkBox; +import com.coremedia.iso.boxes.StaticChunkOffsetBox; +import com.coremedia.iso.boxes.SyncSampleBox; +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.coremedia.iso.boxes.TrackBox; +import com.coremedia.iso.boxes.TrackHeaderBox; +import com.googlecode.mp4parser.authoring.DateHelper; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Creates a plain MP4 file from a video. Plain as plain can be. + */ +public class DefaultMp4Builder implements Mp4Builder { + + public int STEPSIZE = 64; + Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>(); + private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName()); + + HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>(); + HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>(); + private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder(); + + public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { + this.intersectionFinder = intersectionFinder; + } + + /** + * {@inheritDoc} + */ + public IsoFile build(Movie movie) { + LOG.fine("Creating movie " + movie); + for (Track track : movie.getTracks()) { + // getting the samples may be a time consuming activity + List<ByteBuffer> samples = track.getSamples(); + putSamples(track, samples); + long[] sizes = new long[samples.size()]; + for (int i = 0; i < sizes.length; i++) { + sizes[i] = samples.get(i).limit(); + } + putSampleSizes(track, sizes); + } + + IsoFile isoFile = new IsoFile(); + // ouch that is ugly but I don't know how to do it else + List<String> minorBrands = new LinkedList<String>(); + minorBrands.add("isom"); + minorBrands.add("iso2"); + minorBrands.add("avc1"); + + isoFile.addBox(new FileTypeBox("isom", 0, minorBrands)); + isoFile.addBox(createMovieBox(movie)); + InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie); + isoFile.addBox(mdat); + + /* + dataOffset is where the first sample starts. In this special mdat the samples always start + at offset 16 so that we can use the same offset for large boxes and small boxes + */ + long dataOffset = mdat.getDataOffset(); + for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) { + long[] offsets = chunkOffsetBox.getChunkOffsets(); + for (int i = 0; i < offsets.length; i++) { + offsets[i] += dataOffset; + } + } + + + return isoFile; + } + + public FragmentIntersectionFinder getFragmentIntersectionFinder() { + throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!"); + } + + protected long[] putSampleSizes(Track track, long[] sizes) { + return track2SampleSizes.put(track, sizes); + } + + protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) { + return track2Sample.put(track, samples); + } + + private MovieBox createMovieBox(Movie movie) { + MovieBox movieBox = new MovieBox(); + MovieHeaderBox mvhd = new MovieHeaderBox(); + + mvhd.setCreationTime(DateHelper.convert(new Date())); + mvhd.setModificationTime(DateHelper.convert(new Date())); + + long movieTimeScale = getTimescale(movie); + long duration = 0; + + for (Track track : movie.getTracks()) { + long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); + if (tracksDuration > duration) { + duration = tracksDuration; + } + + + } + + mvhd.setDuration(duration); + mvhd.setTimescale(movieTimeScale); + // find the next available trackId + long nextTrackId = 0; + for (Track track : movie.getTracks()) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + mvhd.setNextTrackId(++nextTrackId); + if (mvhd.getCreationTime() >= 1l << 32 || + mvhd.getModificationTime() >= 1l << 32 || + mvhd.getDuration() >= 1l << 32) { + mvhd.setVersion(1); + } + + movieBox.addBox(mvhd); + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrackBox(track, movie)); + } + // metadata here + Box udta = createUdta(movie); + if (udta != null) { + movieBox.addBox(udta); + } + return movieBox; + + } + + /** + * Override to create a user data box that may contain metadata. + * + * @return a 'udta' box or <code>null</code> if none provided + */ + protected Box createUdta(Movie movie) { + return null; + } + + private TrackBox createTrackBox(Track track, Movie movie) { + + LOG.info("Creating Mp4TrackImpl " + track); + TrackBox trackBox = new TrackBox(); + TrackHeaderBox tkhd = new TrackHeaderBox(); + int flags = 0; + if (track.isEnabled()) { + flags += 1; + } + + if (track.isInMovie()) { + flags += 2; + } + + if (track.isInPreview()) { + flags += 4; + } + + if (track.isInPoster()) { + flags += 8; + } + tkhd.setFlags(flags); + + tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); + tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + // We need to take edit list box into account in trackheader duration + // but as long as I don't support edit list boxes it is sufficient to + // just translate media duration to movie timescale + tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale()); + tkhd.setHeight(track.getTrackMetaData().getHeight()); + tkhd.setWidth(track.getTrackMetaData().getWidth()); + tkhd.setLayer(track.getTrackMetaData().getLayer()); + tkhd.setModificationTime(DateHelper.convert(new Date())); + tkhd.setTrackId(track.getTrackMetaData().getTrackId()); + tkhd.setVolume(track.getTrackMetaData().getVolume()); + if (tkhd.getCreationTime() >= 1l << 32 || + tkhd.getModificationTime() >= 1l << 32 || + tkhd.getDuration() >= 1l << 32) { + tkhd.setVersion(1); + } + + trackBox.addBox(tkhd); + +/* + EditBox edit = new EditBox(); + EditListBox editListBox = new EditListBox(); + editListBox.setEntries(Collections.singletonList( + new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1))); + edit.addBox(editListBox); + trackBox.addBox(edit); +*/ + + MediaBox mdia = new MediaBox(); + trackBox.addBox(mdia); + MediaHeaderBox mdhd = new MediaHeaderBox(); + mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + mdhd.setDuration(getDuration(track)); + mdhd.setTimescale(track.getTrackMetaData().getTimescale()); + mdhd.setLanguage(track.getTrackMetaData().getLanguage()); + mdia.addBox(mdhd); + HandlerBox hdlr = new HandlerBox(); + mdia.addBox(hdlr); + + hdlr.setHandlerType(track.getHandler()); + + MediaInformationBox minf = new MediaInformationBox(); + minf.addBox(track.getMediaHeaderBox()); + + // dinf: all these three boxes tell us is that the actual + // data is in the current file and not somewhere external + DataInformationBox dinf = new DataInformationBox(); + DataReferenceBox dref = new DataReferenceBox(); + dinf.addBox(dref); + DataEntryUrlBox url = new DataEntryUrlBox(); + url.setFlags(1); + dref.addBox(url); + minf.addBox(dinf); + // + + SampleTableBox stbl = new SampleTableBox(); + + stbl.addBox(track.getSampleDescriptionBox()); + + List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries(); + if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) { + TimeToSampleBox stts = new TimeToSampleBox(); + stts.setEntries(track.getDecodingTimeEntries()); + stbl.addBox(stts); + } + + List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries(); + if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) { + CompositionTimeToSample ctts = new CompositionTimeToSample(); + ctts.setEntries(compositionTimeToSampleEntries); + stbl.addBox(ctts); + } + + long[] syncSamples = track.getSyncSamples(); + if (syncSamples != null && syncSamples.length > 0) { + SyncSampleBox stss = new SyncSampleBox(); + stss.setSampleNumber(syncSamples); + stbl.addBox(stss); + } + + if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { + SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox(); + sdtp.setEntries(track.getSampleDependencies()); + stbl.addBox(sdtp); + } + HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>(); + for (Track current : movie.getTracks()) { + track2ChunkSizes.put(current, getChunkSizes(current, movie)); + } + int[] tracksChunkSizes = track2ChunkSizes.get(track); + + SampleToChunkBox stsc = new SampleToChunkBox(); + stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>()); + long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size + for (int i = 0; i < tracksChunkSizes.length; i++) { + // The sample description index references the sample description box + // that describes the samples of this chunk. My Tracks cannot have more + // than one sample description box. Therefore 1 is always right + // the first chunk has the number '1' + if (lastChunkSize != tracksChunkSizes[i]) { + stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1)); + lastChunkSize = tracksChunkSizes[i]; + } + } + stbl.addBox(stsc); + + SampleSizeBox stsz = new SampleSizeBox(); + stsz.setSampleSizes(track2SampleSizes.get(track)); + + stbl.addBox(stsz); + // The ChunkOffsetBox we create here is just a stub + // since we haven't created the whole structure we can't tell where the + // first chunk starts (mdat box). So I just let the chunk offset + // start at zero and I will add the mdat offset later. + StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); + this.chunkOffsetBoxes.add(stco); + long offset = 0; + long[] chunkOffset = new long[tracksChunkSizes.length]; + // all tracks have the same number of chunks + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId()); + } + + + for (int i = 0; i < tracksChunkSizes.length; i++) { + // The filelayout will be: + // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n + // calculating the offsets + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i); + } + for (Track current : movie.getTracks()) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId()); + } + int[] chunkSizes = track2ChunkSizes.get(current); + long firstSampleOfChunk = 0; + for (int j = 0; j < i; j++) { + firstSampleOfChunk += chunkSizes[j]; + } + if (current == track) { + chunkOffset[i] = offset; + } + for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { + offset += track2SampleSizes.get(current)[j]; + } + } + } + stco.setChunkOffsets(chunkOffset); + stbl.addBox(stco); + minf.addBox(stbl); + mdia.addBox(minf); + + return trackBox; + } + + private class InterleaveChunkMdat implements Box { + List<Track> tracks; + List<ByteBuffer> samples = new ArrayList<ByteBuffer>(); + ContainerBox parent; + + long contentSize = 0; + + public ContainerBox getParent() { + return parent; + } + + public void setParent(ContainerBox parent) { + this.parent = parent; + } + + public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { + } + + private InterleaveChunkMdat(Movie movie) { + + tracks = movie.getTracks(); + Map<Track, int[]> chunks = new HashMap<Track, int[]>(); + for (Track track : movie.getTracks()) { + chunks.put(track, getChunkSizes(track, movie)); + } + + for (int i = 0; i < chunks.values().iterator().next().length; i++) { + for (Track track : tracks) { + + int[] chunkSizes = chunks.get(track); + long firstSampleOfChunk = 0; + for (int j = 0; j < i; j++) { + firstSampleOfChunk += chunkSizes[j]; + } + + for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { + + ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j); + contentSize += s.limit(); + samples.add((ByteBuffer) s.rewind()); + } + + } + + } + + } + + public long getDataOffset() { + Box b = this; + long offset = 16; + while (b.getParent() != null) { + for (Box box : b.getParent().getBoxes()) { + if (b == box) { + break; + } + offset += box.getSize(); + } + b = b.getParent(); + } + return offset; + } + + + public String getType() { + return "mdat"; + } + + public long getSize() { + return 16 + contentSize; + } + + private boolean isSmallBox(long contentSize) { + return (contentSize + 8) < 4294967296L; + } + + + public void getBox(WritableByteChannel writableByteChannel) throws IOException { + ByteBuffer bb = ByteBuffer.allocate(16); + long size = getSize(); + if (isSmallBox(size)) { + IsoTypeWriter.writeUInt32(bb, size); + } else { + IsoTypeWriter.writeUInt32(bb, 1); + } + bb.put(IsoFile.fourCCtoBytes("mdat")); + if (isSmallBox(size)) { + bb.put(new byte[8]); + } else { + IsoTypeWriter.writeUInt64(bb, size); + } + bb.rewind(); + writableByteChannel.write(bb); + if (writableByteChannel instanceof GatheringByteChannel) { + List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples); + + + for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) { + List<ByteBuffer> sublist = nuSamples.subList( + i * STEPSIZE, // start + (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end + ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); + do { + ((GatheringByteChannel) writableByteChannel).write(sampleArray); + } while (sampleArray[sampleArray.length - 1].remaining() > 0); + } + //System.err.println(bytesWritten); + } else { + for (ByteBuffer sample : samples) { + sample.rewind(); + writableByteChannel.write(sample); + } + } + } + + } + + /** + * Gets the chunk sizes for the given track. + * + * @param track + * @param movie + * @return + */ + int[] getChunkSizes(Track track, Movie movie) { + + long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie); + int[] chunkSizes = new int[referenceChunkStarts.length]; + + + for (int i = 0; i < referenceChunkStarts.length; i++) { + long start = referenceChunkStarts[i] - 1; + long end; + if (referenceChunkStarts.length == i + 1) { + end = track.getSamples().size(); + } else { + end = referenceChunkStarts[i + 1] - 1; + } + + chunkSizes[i] = l2i(end - start); + // The Stretch makes sure that there are as much audio and video chunks! + } + assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal"; + return chunkSizes; + + + } + + + private static long sum(int[] ls) { + long rc = 0; + for (long l : ls) { + rc += l; + } + return rc; + } + + protected static long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + public long getTimescale(Movie movie) { + long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale(); + for (Track track : movie.getTracks()) { + timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); + } + return timescale; + } + + public static long gcd(long a, long b) { + if (b == 0) { + return a; + } + return gcd(b, a % b); + } + + public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) { + ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); + for (ByteBuffer buffer : samples) { + int lastIndex = nuSamples.size() - 1; + if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && + nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { + ByteBuffer oldBuffer = nuSamples.remove(lastIndex); + ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); + // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. + nuSamples.add(nu); + } else if (lastIndex >= 0 && + buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && + nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { + // This can go wrong - but will it? + ByteBuffer oldBuffer = nuSamples.get(lastIndex); + oldBuffer.limit(buffer.limit() + oldBuffer.limit()); + } else { + nuSamples.add(buffer); + } + } + return nuSamples; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentIntersectionFinder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentIntersectionFinder.java new file mode 100644 index 0000000..1224bbf --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentIntersectionFinder.java @@ -0,0 +1,34 @@ +/* + * 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.builder; + +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +/** + * + */ +public interface FragmentIntersectionFinder { + /** + * Gets the ordinal number of the samples which will be the first sample + * in each fragment. + * + * @param track concerned track + * @param movie the context of the track + * @return an array containing the ordinal of each fragment's first sample + */ + public long[] sampleNumbers(Track track, Movie movie); +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentedMp4Builder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentedMp4Builder.java new file mode 100644 index 0000000..c65ff1c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/FragmentedMp4Builder.java @@ -0,0 +1,742 @@ +/* + * 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.builder; + +import com.coremedia.iso.BoxParser; +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.IsoTypeWriter; +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.fragment.*; +import com.googlecode.mp4parser.authoring.DateHelper; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.*; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Creates a fragmented MP4 file. + */ +public class FragmentedMp4Builder implements Mp4Builder { + private static final Logger LOG = Logger.getLogger(FragmentedMp4Builder.class.getName()); + + protected FragmentIntersectionFinder intersectionFinder; + + public FragmentedMp4Builder() { + this.intersectionFinder = new SyncSampleIntersectFinderImpl(); + } + + public List<String> getAllowedHandlers() { + return Arrays.asList("soun", "vide"); + } + + public Box createFtyp(Movie movie) { + List<String> minorBrands = new LinkedList<String>(); + minorBrands.add("isom"); + minorBrands.add("iso2"); + minorBrands.add("avc1"); + return new FileTypeBox("isom", 0, minorBrands); + } + + /** + * Some formats require sorting of the fragments. E.g. Ultraviolet CFF files are required + * to contain the fragments size sort: + * <ul> + * <li>video[1].getBytes().length < audio[1].getBytes().length < subs[1].getBytes().length</li> + * <li> audio[2].getBytes().length < video[2].getBytes().length < subs[2].getBytes().length</li> + * </ul> + * + * make this fragment: + * + * <ol> + * <li>video[1]</li> + * <li>audio[1]</li> + * <li>subs[1]</li> + * <li>audio[2]</li> + * <li>video[2]</li> + * <li>subs[2]</li> + * </ol> + * + * @param tracks the list of tracks to returned sorted + * @param cycle current fragment (sorting may vary between the fragments) + * @param intersectionMap a map from tracks to their fragments' first samples. + * @return the list of tracks in order of appearance in the fragment + */ + protected List<Track> sortTracksInSequence(List<Track> tracks, final int cycle, final Map<Track, long[]> intersectionMap) { + tracks = new LinkedList<Track>(tracks); + Collections.sort(tracks, new Comparator<Track>() { + public int compare(Track o1, Track o2) { + long[] startSamples1 = intersectionMap.get(o1); + long startSample1 = startSamples1[cycle]; + // one based sample numbers - the first sample is 1 + long endSample1 = cycle + 1 < startSamples1.length ? startSamples1[cycle + 1] : o1.getSamples().size() + 1; + long[] startSamples2 = intersectionMap.get(o2); + long startSample2 = startSamples2[cycle]; + // one based sample numbers - the first sample is 1 + long endSample2 = cycle + 1 < startSamples2.length ? startSamples2[cycle + 1] : o2.getSamples().size() + 1; + List<ByteBuffer> samples1 = o1.getSamples().subList(l2i(startSample1) - 1, l2i(endSample1) - 1); + List<ByteBuffer> samples2 = o2.getSamples().subList(l2i(startSample2) - 1, l2i(endSample2) - 1); + int size1 = 0; + for (ByteBuffer byteBuffer : samples1) { + size1 += byteBuffer.limit(); + } + int size2 = 0; + for (ByteBuffer byteBuffer : samples2) { + size2 += byteBuffer.limit(); + } + return size1 - size2; + } + }); + return tracks; + } + + protected List<Box> createMoofMdat(final Movie movie) { + List<Box> boxes = new LinkedList<Box>(); + HashMap<Track, long[]> intersectionMap = new HashMap<Track, long[]>(); + int maxNumberOfFragments = 0; + for (Track track : movie.getTracks()) { + long[] intersects = intersectionFinder.sampleNumbers(track, movie); + intersectionMap.put(track, intersects); + maxNumberOfFragments = Math.max(maxNumberOfFragments, intersects.length); + } + + + int sequence = 1; + // this loop has two indices: + + for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) { + + final List<Track> sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap); + + for (Track track : sortedTracks) { + if (getAllowedHandlers().isEmpty() || getAllowedHandlers().contains(track.getHandler())) { + long[] startSamples = intersectionMap.get(track); + //some tracks may have less fragments -> skip them + if (cycle < startSamples.length) { + + long startSample = startSamples[cycle]; + // one based sample numbers - the first sample is 1 + long endSample = cycle + 1 < startSamples.length ? startSamples[cycle + 1] : track.getSamples().size() + 1; + + // if startSample == endSample the cycle is empty! + if (startSample != endSample) { + boxes.add(createMoof(startSample, endSample, track, sequence)); + boxes.add(createMdat(startSample, endSample, track, sequence++)); + } + } + } + } + } + return boxes; + } + + /** + * {@inheritDoc} + */ + public IsoFile build(Movie movie) { + LOG.fine("Creating movie " + movie); + IsoFile isoFile = new IsoFile(); + + + isoFile.addBox(createFtyp(movie)); + isoFile.addBox(createMoov(movie)); + + for (Box box : createMoofMdat(movie)) { + isoFile.addBox(box); + } + isoFile.addBox(createMfra(movie, isoFile)); + + return isoFile; + } + + protected Box createMdat(final long startSample, final long endSample, final Track track, final int i) { + + class Mdat implements Box { + ContainerBox parent; + + public ContainerBox getParent() { + return parent; + } + + public void setParent(ContainerBox parent) { + this.parent = parent; + } + + public long getSize() { + long size = 8; // I don't expect 2gig fragments + for (ByteBuffer sample : getSamples(startSample, endSample, track, i)) { + size += sample.limit(); + } + return size; + } + + public String getType() { + return "mdat"; + } + + public void getBox(WritableByteChannel writableByteChannel) throws IOException { + List<ByteBuffer> bbs = getSamples(startSample, endSample, track, i); + final List<ByteBuffer> samples = ByteBufferHelper.mergeAdjacentBuffers(bbs); + ByteBuffer header = ByteBuffer.allocate(8); + IsoTypeWriter.writeUInt32(header, l2i(getSize())); + header.put(IsoFile.fourCCtoBytes(getType())); + header.rewind(); + writableByteChannel.write(header); + if (writableByteChannel instanceof GatheringByteChannel) { + + int STEPSIZE = 1024; + // This is required to prevent android from crashing + // it seems that {@link GatheringByteChannel#write(java.nio.ByteBuffer[])} + // just handles up to 1024 buffers + for (int i = 0; i < Math.ceil((double) samples.size() / STEPSIZE); i++) { + List<ByteBuffer> sublist = samples.subList( + i * STEPSIZE, // start + (i + 1) * STEPSIZE < samples.size() ? (i + 1) * STEPSIZE : samples.size()); // end + ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); + do { + ((GatheringByteChannel) writableByteChannel).write(sampleArray); + } while (sampleArray[sampleArray.length - 1].remaining() > 0); + } + //System.err.println(bytesWritten); + } else { + for (ByteBuffer sample : samples) { + sample.rewind(); + writableByteChannel.write(sample); + } + } + + } + + public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { + + } + } + + return new Mdat(); + } + + protected Box createTfhd(long startSample, long endSample, Track track, int sequenceNumber) { + TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox(); + SampleFlags sf = new SampleFlags(); + + tfhd.setDefaultSampleFlags(sf); + tfhd.setBaseDataOffset(-1); + tfhd.setTrackId(track.getTrackMetaData().getTrackId()); + return tfhd; + } + + protected Box createMfhd(long startSample, long endSample, Track track, int sequenceNumber) { + MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox(); + mfhd.setSequenceNumber(sequenceNumber); + return mfhd; + } + + protected Box createTraf(long startSample, long endSample, Track track, int sequenceNumber) { + TrackFragmentBox traf = new TrackFragmentBox(); + traf.addBox(createTfhd(startSample, endSample, track, sequenceNumber)); + for (Box trun : createTruns(startSample, endSample, track, sequenceNumber)) { + traf.addBox(trun); + } + + return traf; + } + + + /** + * Gets the all samples starting with <code>startSample</code> (one based -> one is the first) and + * ending with <code>endSample</code> (exclusive). + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return a <code>List<ByteBuffer></code> of raw samples + */ + protected List<ByteBuffer> getSamples(long startSample, long endSample, Track track, int sequenceNumber) { + // since startSample and endSample are one-based substract 1 before addressing list elements + return track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); + } + + /** + * Gets the sizes of a sequence of samples- + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return + */ + protected long[] getSampleSizes(long startSample, long endSample, Track track, int sequenceNumber) { + List<ByteBuffer> samples = getSamples(startSample, endSample, track, sequenceNumber); + + long[] sampleSizes = new long[samples.size()]; + for (int i = 0; i < sampleSizes.length; i++) { + sampleSizes[i] = samples.get(i).limit(); + } + return sampleSizes; + } + + /** + * Creates one or more track run boxes for a given sequence. + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return the list of TrackRun boxes. + */ + protected List<? extends Box> createTruns(long startSample, long endSample, Track track, int sequenceNumber) { + TrackRunBox trun = new TrackRunBox(); + long[] sampleSizes = getSampleSizes(startSample, endSample, track, sequenceNumber); + + trun.setSampleDurationPresent(true); + trun.setSampleSizePresent(true); + List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(l2i(endSample - startSample)); + + + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + long left = startSample - 1; + long curEntryLeft = timeQueue.peek().getCount(); + while (left > curEntryLeft) { + left -= curEntryLeft; + timeQueue.remove(); + curEntryLeft = timeQueue.peek().getCount(); + } + curEntryLeft -= left; + + + Queue<CompositionTimeToSample.Entry> compositionTimeQueue = + track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0 ? + new LinkedList<CompositionTimeToSample.Entry>(track.getCompositionTimeEntries()) : null; + long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue.peek().getCount() : -1; + + + trun.setSampleCompositionTimeOffsetPresent(compositionTimeEntriesLeft > 0); + + // fast forward composition stuff + for (long i = 1; i < startSample; i++) { + if (compositionTimeQueue != null) { + //trun.setSampleCompositionTimeOffsetPresent(true); + if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { + compositionTimeQueue.remove(); + compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); + } + } + } + + boolean sampleFlagsRequired = (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() || + track.getSyncSamples() != null && track.getSyncSamples().length != 0); + + trun.setSampleFlagsPresent(sampleFlagsRequired); + + for (int i = 0; i < sampleSizes.length; i++) { + TrackRunBox.Entry entry = new TrackRunBox.Entry(); + entry.setSampleSize(sampleSizes[i]); + if (sampleFlagsRequired) { + //if (false) { + SampleFlags sflags = new SampleFlags(); + + if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { + SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i); + sflags.setSampleDependsOn(e.getSampleDependsOn()); + sflags.setSampleIsDependedOn(e.getSampleIsDependentOn()); + sflags.setSampleHasRedundancy(e.getSampleHasRedundancy()); + } + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + // we have to mark non-sync samples! + if (Arrays.binarySearch(track.getSyncSamples(), startSample + i) >= 0) { + sflags.setSampleIsDifferenceSample(false); + sflags.setSampleDependsOn(2); + } else { + sflags.setSampleIsDifferenceSample(true); + sflags.setSampleDependsOn(1); + } + } + // i don't have sample degradation + entry.setSampleFlags(sflags); + + } + + entry.setSampleDuration(timeQueue.peek().getDelta()); + if (--curEntryLeft == 0 && timeQueue.size() > 1) { + timeQueue.remove(); + curEntryLeft = timeQueue.peek().getCount(); + } + + if (compositionTimeQueue != null) { + entry.setSampleCompositionTimeOffset(compositionTimeQueue.peek().getOffset()); + if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { + compositionTimeQueue.remove(); + compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); + } + } + entries.add(entry); + } + + trun.setEntries(entries); + + return Collections.singletonList(trun); + } + + /** + * Creates a 'moof' box for a given sequence of samples. + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return the list of TrackRun boxes. + */ + protected Box createMoof(long startSample, long endSample, Track track, int sequenceNumber) { + MovieFragmentBox moof = new MovieFragmentBox(); + moof.addBox(createMfhd(startSample, endSample, track, sequenceNumber)); + moof.addBox(createTraf(startSample, endSample, track, sequenceNumber)); + + TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0); + firstTrun.setDataOffset(1); // dummy to make size correct + firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size + + return moof; + } + + /** + * Creates a single 'mvhd' movie header box for a given movie. + * + * @param movie the concerned movie + * @return an 'mvhd' box + */ + protected Box createMvhd(Movie movie) { + MovieHeaderBox mvhd = new MovieHeaderBox(); + mvhd.setVersion(1); + mvhd.setCreationTime(DateHelper.convert(new Date())); + mvhd.setModificationTime(DateHelper.convert(new Date())); + long movieTimeScale = movie.getTimescale(); + long duration = 0; + + for (Track track : movie.getTracks()) { + long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); + if (tracksDuration > duration) { + duration = tracksDuration; + } + + + } + + mvhd.setDuration(duration); + mvhd.setTimescale(movieTimeScale); + // find the next available trackId + long nextTrackId = 0; + for (Track track : movie.getTracks()) { + nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; + } + mvhd.setNextTrackId(++nextTrackId); + return mvhd; + } + + /** + * Creates a fully populated 'moov' box with all child boxes. Child boxes are: + * <ul> + * <li>{@link #createMvhd(com.googlecode.mp4parser.authoring.Movie) mvhd}</li> + * <li>{@link #createMvex(com.googlecode.mp4parser.authoring.Movie) mvex}</li> + * <li>a {@link #createTrak(com.googlecode.mp4parser.authoring.Track, com.googlecode.mp4parser.authoring.Movie) trak} for every track</li> + * </ul> + * + * @param movie the concerned movie + * @return fully populated 'moov' + */ + protected Box createMoov(Movie movie) { + MovieBox movieBox = new MovieBox(); + + movieBox.addBox(createMvhd(movie)); + movieBox.addBox(createMvex(movie)); + + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrak(track, movie)); + } + // metadata here + return movieBox; + + } + + /** + * Creates a 'tfra' - track fragment random access box for the given track with the isoFile. + * The tfra contains a map of random access points with time as key and offset within the isofile + * as value. + * + * @param track the concerned track + * @param isoFile the track is contained in + * @return a track fragment random access box. + */ + protected Box createTfra(Track track, IsoFile isoFile) { + TrackFragmentRandomAccessBox tfra = new TrackFragmentRandomAccessBox(); + tfra.setVersion(1); // use long offsets and times + List<TrackFragmentRandomAccessBox.Entry> offset2timeEntries = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); + List<Box> boxes = isoFile.getBoxes(); + long offset = 0; + long duration = 0; + for (Box box : boxes) { + if (box instanceof MovieFragmentBox) { + List<TrackFragmentBox> trafs = ((MovieFragmentBox) box).getBoxes(TrackFragmentBox.class); + for (int i = 0; i < trafs.size(); i++) { + TrackFragmentBox traf = trafs.get(i); + if (traf.getTrackFragmentHeaderBox().getTrackId() == track.getTrackMetaData().getTrackId()) { + // here we are at the offset required for the current entry. + List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); + for (int j = 0; j < truns.size(); j++) { + List<TrackFragmentRandomAccessBox.Entry> offset2timeEntriesThisTrun = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); + TrackRunBox trun = truns.get(j); + for (int k = 0; k < trun.getEntries().size(); k++) { + TrackRunBox.Entry trunEntry = trun.getEntries().get(k); + SampleFlags sf = null; + if (k == 0 && trun.isFirstSampleFlagsPresent()) { + sf = trun.getFirstSampleFlags(); + } else if (trun.isSampleFlagsPresent()) { + sf = trunEntry.getSampleFlags(); + } else { + List<MovieExtendsBox> mvexs = isoFile.getMovieBox().getBoxes(MovieExtendsBox.class); + for (MovieExtendsBox mvex : mvexs) { + List<TrackExtendsBox> trexs = mvex.getBoxes(TrackExtendsBox.class); + for (TrackExtendsBox trex : trexs) { + if (trex.getTrackId() == track.getTrackMetaData().getTrackId()) { + sf = trex.getDefaultSampleFlags(); + } + } + } + + } + if (sf == null) { + throw new RuntimeException("Could not find any SampleFlags to indicate random access or not"); + } + if (sf.getSampleDependsOn() == 2) { + offset2timeEntriesThisTrun.add(new TrackFragmentRandomAccessBox.Entry( + duration, + offset, + i + 1, j + 1, k + 1)); + } + duration += trunEntry.getSampleDuration(); + } + if (offset2timeEntriesThisTrun.size() == trun.getEntries().size() && trun.getEntries().size() > 0) { + // Oooops every sample seems to be random access sample + // is this an audio track? I don't care. + // I just use the first for trun sample for tfra random access + offset2timeEntries.add(offset2timeEntriesThisTrun.get(0)); + } else { + offset2timeEntries.addAll(offset2timeEntriesThisTrun); + } + } + } + } + } + + + offset += box.getSize(); + } + tfra.setEntries(offset2timeEntries); + tfra.setTrackId(track.getTrackMetaData().getTrackId()); + return tfra; + } + + /** + * Creates a 'mfra' - movie fragment random access box for the given movie in the given + * isofile. Uses {@link #createTfra(com.googlecode.mp4parser.authoring.Track, com.coremedia.iso.IsoFile)} + * to generate the child boxes. + * + * @param movie concerned movie + * @param isoFile concerned isofile + * @return a complete 'mfra' box + */ + protected Box createMfra(Movie movie, IsoFile isoFile) { + MovieFragmentRandomAccessBox mfra = new MovieFragmentRandomAccessBox(); + for (Track track : movie.getTracks()) { + mfra.addBox(createTfra(track, isoFile)); + } + + MovieFragmentRandomAccessOffsetBox mfro = new MovieFragmentRandomAccessOffsetBox(); + mfra.addBox(mfro); + mfro.setMfraSize(mfra.getSize()); + return mfra; + } + + protected Box createTrex(Movie movie, Track track) { + TrackExtendsBox trex = new TrackExtendsBox(); + trex.setTrackId(track.getTrackMetaData().getTrackId()); + trex.setDefaultSampleDescriptionIndex(1); + trex.setDefaultSampleDuration(0); + trex.setDefaultSampleSize(0); + SampleFlags sf = new SampleFlags(); + if ("soun".equals(track.getHandler())) { + // as far as I know there is no audio encoding + // where the sample are not self contained. + sf.setSampleDependsOn(2); + sf.setSampleIsDependedOn(2); + } + trex.setDefaultSampleFlags(sf); + return trex; + } + + /** + * Creates a 'mvex' - movie extends box and populates it with 'trex' boxes + * by calling {@link #createTrex(com.googlecode.mp4parser.authoring.Movie, com.googlecode.mp4parser.authoring.Track)} + * for each track to generate them + * + * @param movie the source movie + * @return a complete 'mvex' + */ + protected Box createMvex(Movie movie) { + MovieExtendsBox mvex = new MovieExtendsBox(); + final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox(); + for (Track track : movie.getTracks()) { + final long trackDuration = getTrackDuration(movie, track); + if (mved.getFragmentDuration() < trackDuration) { + mved.setFragmentDuration(trackDuration); + } + } + mvex.addBox(mved); + + for (Track track : movie.getTracks()) { + mvex.addBox(createTrex(movie, track)); + } + return mvex; + } + + protected Box createTkhd(Movie movie, Track track) { + TrackHeaderBox tkhd = new TrackHeaderBox(); + tkhd.setVersion(1); + int flags = 0; + if (track.isEnabled()) { + flags += 1; + } + + if (track.isInMovie()) { + flags += 2; + } + + if (track.isInPreview()) { + flags += 4; + } + + if (track.isInPoster()) { + flags += 8; + } + tkhd.setFlags(flags); + + tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); + tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + // We need to take edit list box into account in trackheader duration + // but as long as I don't support edit list boxes it is sufficient to + // just translate media duration to movie timescale + tkhd.setDuration(getTrackDuration(movie, track)); + tkhd.setHeight(track.getTrackMetaData().getHeight()); + tkhd.setWidth(track.getTrackMetaData().getWidth()); + tkhd.setLayer(track.getTrackMetaData().getLayer()); + tkhd.setModificationTime(DateHelper.convert(new Date())); + tkhd.setTrackId(track.getTrackMetaData().getTrackId()); + tkhd.setVolume(track.getTrackMetaData().getVolume()); + return tkhd; + } + + private long getTrackDuration(Movie movie, Track track) { + return getDuration(track) * movie.getTimescale() / track.getTrackMetaData().getTimescale(); + } + + protected Box createMdhd(Movie movie, Track track) { + MediaHeaderBox mdhd = new MediaHeaderBox(); + mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); + mdhd.setDuration(getDuration(track)); + mdhd.setTimescale(track.getTrackMetaData().getTimescale()); + mdhd.setLanguage(track.getTrackMetaData().getLanguage()); + return mdhd; + } + + protected Box createStbl(Movie movie, Track track) { + SampleTableBox stbl = new SampleTableBox(); + + stbl.addBox(track.getSampleDescriptionBox()); + stbl.addBox(new TimeToSampleBox()); + //stbl.addBox(new SampleToChunkBox()); + stbl.addBox(new StaticChunkOffsetBox()); + return stbl; + } + + protected Box createMinf(Track track, Movie movie) { + MediaInformationBox minf = new MediaInformationBox(); + minf.addBox(track.getMediaHeaderBox()); + minf.addBox(createDinf(movie, track)); + minf.addBox(createStbl(movie, track)); + return minf; + } + + protected Box createMdiaHdlr(Track track, Movie movie) { + HandlerBox hdlr = new HandlerBox(); + hdlr.setHandlerType(track.getHandler()); + return hdlr; + } + + protected Box createMdia(Track track, Movie movie) { + MediaBox mdia = new MediaBox(); + mdia.addBox(createMdhd(movie, track)); + + + mdia.addBox(createMdiaHdlr(track, movie)); + + + mdia.addBox(createMinf(track, movie)); + return mdia; + } + + protected Box createTrak(Track track, Movie movie) { + LOG.fine("Creating Track " + track); + TrackBox trackBox = new TrackBox(); + trackBox.addBox(createTkhd(movie, track)); + trackBox.addBox(createMdia(track, movie)); + return trackBox; + } + + protected DataInformationBox createDinf(Movie movie, Track track) { + DataInformationBox dinf = new DataInformationBox(); + DataReferenceBox dref = new DataReferenceBox(); + dinf.addBox(dref); + DataEntryUrlBox url = new DataEntryUrlBox(); + url.setFlags(1); + dref.addBox(url); + return dinf; + } + + public FragmentIntersectionFinder getFragmentIntersectionFinder() { + return intersectionFinder; + } + + public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { + this.intersectionFinder = intersectionFinder; + } + + protected long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/Mp4Builder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/Mp4Builder.java new file mode 100644 index 0000000..725745e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/Mp4Builder.java @@ -0,0 +1,35 @@ +/* + * 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.builder; + +import com.coremedia.iso.IsoFile; +import com.googlecode.mp4parser.authoring.Movie; + +/** + * Transforms a <code>Movie</code> object to an IsoFile. Implementations can + * determine the specific format: Fragmented MP4, MP4, MP4 with Apple Metadata, + * MP4 with 3GPP Metadata, MOV. + */ +public interface Mp4Builder { + /** + * Builds the actual IsoFile from the Movie. + * + * @param movie data source + * @return the freshly built IsoFile + */ + public IsoFile build(Movie movie); + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/SyncSampleIntersectFinderImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/SyncSampleIntersectFinderImpl.java new file mode 100644 index 0000000..2766c5e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/SyncSampleIntersectFinderImpl.java @@ -0,0 +1,334 @@ +/* + * 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.builder; + +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import static com.googlecode.mp4parser.util.Math.lcm; + +/** + * This <code>FragmentIntersectionFinder</code> cuts the input movie video tracks in + * fragments of the same length exactly before the sync samples. Audio tracks are cut + * into pieces of similar length. + */ +public class SyncSampleIntersectFinderImpl implements FragmentIntersectionFinder { + + private static Logger LOG = Logger.getLogger(SyncSampleIntersectFinderImpl.class.getName()); + private static Map<CacheTuple, long[]> getTimesCache = new ConcurrentHashMap<CacheTuple, long[]>(); + private static Map<CacheTuple, long[]> getSampleNumbersCache = new ConcurrentHashMap<CacheTuple, long[]>(); + + private final int minFragmentDurationSeconds; + + public SyncSampleIntersectFinderImpl() { + minFragmentDurationSeconds = 0; + } + + /** + * Creates a <code>SyncSampleIntersectFinderImpl</code> that will not create any fragment + * smaller than the given <code>minFragmentDurationSeconds</code> + * + * @param minFragmentDurationSeconds the smallest allowable duration of a fragment. + */ + public SyncSampleIntersectFinderImpl(int minFragmentDurationSeconds) { + this.minFragmentDurationSeconds = minFragmentDurationSeconds; + } + + /** + * Gets an array of sample numbers that are meant to be the first sample of each + * chunk or fragment. + * + * @param track concerned track + * @param movie the context of the track + * @return an array containing the ordinal of each fragment's first sample + */ + public long[] sampleNumbers(Track track, Movie movie) { + final CacheTuple key = new CacheTuple(track, movie); + final long[] result = getSampleNumbersCache.get(key); + if (result != null) { + return result; + } + + if ("vide".equals(track.getHandler())) { + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + List<long[]> times = getSyncSamplesTimestamps(movie, track); + final long[] commonIndices = getCommonIndices(track.getSyncSamples(), getTimes(track, movie), track.getTrackMetaData().getTimescale(), times.toArray(new long[times.size()][])); + getSampleNumbersCache.put(key, commonIndices); + return commonIndices; + } else { + throw new RuntimeException("Video Tracks need sync samples. Only tracks other than video may have no sync samples."); + } + } else if ("soun".equals(track.getHandler())) { + Track referenceTrack = null; + for (Track candidate : movie.getTracks()) { + if (candidate.getSyncSamples() != null && "vide".equals(candidate.getHandler()) && candidate.getSyncSamples().length > 0) { + referenceTrack = candidate; + } + } + if (referenceTrack != null) { + + // Gets the reference track's fra + long[] refSyncSamples = sampleNumbers(referenceTrack, movie); + + int refSampleCount = referenceTrack.getSamples().size(); + + long[] syncSamples = new long[refSyncSamples.length]; + long minSampleRate = 192000; + for (Track testTrack : movie.getTracks()) { + if ("soun".equals(testTrack.getHandler())) { + AudioSampleEntry ase = (AudioSampleEntry) testTrack.getSampleDescriptionBox().getSampleEntry(); + if (ase.getSampleRate() < minSampleRate) { + minSampleRate = ase.getSampleRate(); + long sc = testTrack.getSamples().size(); + double stretch = (double) sc / refSampleCount; + TimeToSampleBox.Entry sttsEntry = testTrack.getDecodingTimeEntries().get(0); + long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types + + for (int i = 0; i < syncSamples.length; i++) { + long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1) * samplesPerFrame); + syncSamples[i] = start; + // The Stretch makes sure that there are as much audio and video chunks! + } + break; + } + } + } + AudioSampleEntry ase = (AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry(); + TimeToSampleBox.Entry sttsEntry = track.getDecodingTimeEntries().get(0); + long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types + double factor = (double) ase.getSampleRate() / (double) minSampleRate; + if (factor != Math.rint(factor)) { // Not an integer + throw new RuntimeException("Sample rates must be a multiple of the lowest sample rate to create a correct file!"); + } + for (int i = 0; i < syncSamples.length; i++) { + syncSamples[i] = (long) (1 + syncSamples[i] * factor / (double) samplesPerFrame); + } + getSampleNumbersCache.put(key, syncSamples); + return syncSamples; + } + throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!"); + } else { + // Ok, my track has no sync samples - let's find one with sync samples. + for (Track candidate : movie.getTracks()) { + if (candidate.getSyncSamples() != null && candidate.getSyncSamples().length > 0) { + long[] refSyncSamples = sampleNumbers(candidate, movie); + int refSampleCount = candidate.getSamples().size(); + + long[] syncSamples = new long[refSyncSamples.length]; + long sc = track.getSamples().size(); + double stretch = (double) sc / refSampleCount; + + for (int i = 0; i < syncSamples.length; i++) { + long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1)) + 1; + syncSamples[i] = start; + // The Stretch makes sure that there are as much audio and video chunks! + } + getSampleNumbersCache.put(key, syncSamples); + return syncSamples; + } + } + throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!"); + } + + + } + + /** + * Calculates the timestamp of all tracks' sync samples. + * + * @param movie + * @param track + * @return + */ + public static List<long[]> getSyncSamplesTimestamps(Movie movie, Track track) { + List<long[]> times = new LinkedList<long[]>(); + for (Track currentTrack : movie.getTracks()) { + if (currentTrack.getHandler().equals(track.getHandler())) { + long[] currentTrackSyncSamples = currentTrack.getSyncSamples(); + if (currentTrackSyncSamples != null && currentTrackSyncSamples.length > 0) { + final long[] currentTrackTimes = getTimes(currentTrack, movie); + times.add(currentTrackTimes); + } + } + } + return times; + } + + public long[] getCommonIndices(long[] syncSamples, long[] syncSampleTimes, long timeScale, long[]... otherTracksTimes) { + List<Long> nuSyncSamples = new LinkedList<Long>(); + List<Long> nuSyncSampleTimes = new LinkedList<Long>(); + + + for (int i = 0; i < syncSampleTimes.length; i++) { + boolean foundInEveryRef = true; + for (long[] times : otherTracksTimes) { + foundInEveryRef &= (Arrays.binarySearch(times, syncSampleTimes[i]) >= 0); + } + + if (foundInEveryRef) { + // use sample only if found in every other track. + nuSyncSamples.add(syncSamples[i]); + nuSyncSampleTimes.add(syncSampleTimes[i]); + } + } + // We have two arrays now: + // nuSyncSamples: Contains all common sync samples + // nuSyncSampleTimes: Contains the times of all sync samples + + // Start: Warn user if samples are not matching! + if (nuSyncSamples.size() < (syncSamples.length * 0.25)) { + String log = ""; + log += String.format("%5d - Common: [", nuSyncSamples.size()); + for (long l : nuSyncSamples) { + log += (String.format("%10d,", l)); + } + log += ("]"); + LOG.warning(log); + log = ""; + + log += String.format("%5d - In : [", syncSamples.length); + for (long l : syncSamples) { + log += (String.format("%10d,", l)); + } + log += ("]"); + LOG.warning(log); + LOG.warning("There are less than 25% of common sync samples in the given track."); + throw new RuntimeException("There are less than 25% of common sync samples in the given track."); + } else if (nuSyncSamples.size() < (syncSamples.length * 0.5)) { + LOG.fine("There are less than 50% of common sync samples in the given track. This is implausible but I'm ok to continue."); + } else if (nuSyncSamples.size() < syncSamples.length) { + LOG.finest("Common SyncSample positions vs. this tracks SyncSample positions: " + nuSyncSamples.size() + " vs. " + syncSamples.length); + } + // End: Warn user if samples are not matching! + + + + + List<Long> finalSampleList = new LinkedList<Long>(); + + if (minFragmentDurationSeconds > 0) { + // if minFragmentDurationSeconds is greater 0 + // we need to throw away certain samples. + long lastSyncSampleTime = -1; + Iterator<Long> nuSyncSamplesIterator = nuSyncSamples.iterator(); + Iterator<Long> nuSyncSampleTimesIterator = nuSyncSampleTimes.iterator(); + while (nuSyncSamplesIterator.hasNext() && nuSyncSampleTimesIterator.hasNext()) { + long curSyncSample = nuSyncSamplesIterator.next(); + long curSyncSampleTime = nuSyncSampleTimesIterator.next(); + if (lastSyncSampleTime == -1 || (curSyncSampleTime - lastSyncSampleTime) / timeScale >= minFragmentDurationSeconds) { + finalSampleList.add(curSyncSample); + lastSyncSampleTime = curSyncSampleTime; + } + } + } else { + // the list of all samples is the final list of samples + // since minFragmentDurationSeconds ist not used. + finalSampleList = nuSyncSamples; + } + + + // transform the list to an array + long[] finalSampleArray = new long[finalSampleList.size()]; + for (int i = 0; i < finalSampleArray.length; i++) { + finalSampleArray[i] = finalSampleList.get(i); + } + return finalSampleArray; + + } + + + private static long[] getTimes(Track track, Movie m) { + final CacheTuple key = new CacheTuple(track, m); + final long[] result = getTimesCache.get(key); + if (result != null) { + return result; + } + + long[] syncSamples = track.getSyncSamples(); + long[] syncSampleTimes = new long[syncSamples.length]; + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + + int currentSample = 1; // first syncsample is 1 + long currentDuration = 0; + long currentDelta = 0; + int currentSyncSampleIndex = 0; + long left = 0; + + final long scalingFactor = calculateTracktimesScalingFactor(m, track); + + while (currentSample <= syncSamples[syncSamples.length - 1]) { + if (currentSample++ == syncSamples[currentSyncSampleIndex]) { + syncSampleTimes[currentSyncSampleIndex++] = currentDuration * scalingFactor; + } + if (left-- == 0) { + TimeToSampleBox.Entry entry = timeQueue.poll(); + left = entry.getCount() - 1; + currentDelta = entry.getDelta(); + } + currentDuration += currentDelta; + } + getTimesCache.put(key, syncSampleTimes); + return syncSampleTimes; + } + + private static long calculateTracktimesScalingFactor(Movie m, Track track) { + long timeScale = 1; + for (Track track1 : m.getTracks()) { + if (track1.getHandler().equals(track.getHandler())) { + if (track1.getTrackMetaData().getTimescale() != track.getTrackMetaData().getTimescale()) { + timeScale = lcm(timeScale, track1.getTrackMetaData().getTimescale()); + } + } + } + return timeScale; + } + + public static class CacheTuple { + Track track; + Movie movie; + + public CacheTuple(Track track, Movie movie) { + this.track = track; + this.movie = movie; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CacheTuple that = (CacheTuple) o; + + if (movie != null ? !movie.equals(that.movie) : that.movie != null) return false; + if (track != null ? !track.equals(that.track) : that.track != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = track != null ? track.hashCode() : 0; + result = 31 * result + (movie != null ? movie.hashCode() : 0); + return result; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/TwoSecondIntersectionFinder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/TwoSecondIntersectionFinder.java new file mode 100644 index 0000000..88aa4ab --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/TwoSecondIntersectionFinder.java @@ -0,0 +1,86 @@ +/* + * 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.builder; + +import com.coremedia.iso.boxes.TimeToSampleBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; + +import java.util.Arrays; +import java.util.List; + +/** + * This <code>FragmentIntersectionFinder</code> cuts the input movie in 2 second + * snippets. + */ +public class TwoSecondIntersectionFinder implements FragmentIntersectionFinder { + + protected long getDuration(Track track) { + long duration = 0; + for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { + duration += entry.getCount() * entry.getDelta(); + } + return duration; + } + + /** + * {@inheritDoc} + */ + public long[] sampleNumbers(Track track, Movie movie) { + List<TimeToSampleBox.Entry> entries = track.getDecodingTimeEntries(); + + double trackLength = 0; + for (Track thisTrack : movie.getTracks()) { + double thisTracksLength = getDuration(thisTrack) / thisTrack.getTrackMetaData().getTimescale(); + if (trackLength < thisTracksLength) { + trackLength = thisTracksLength; + } + } + + int fragmentCount = (int)Math.ceil(trackLength / 2) - 1; + if (fragmentCount < 1) { + fragmentCount = 1; + } + + long fragments[] = new long[fragmentCount]; + Arrays.fill(fragments, -1); + fragments[0] = 1; + + long time = 0; + int samples = 0; + for (TimeToSampleBox.Entry entry : entries) { + for (int i = 0; i < entry.getCount(); i++) { + int currentFragment = (int) (time / track.getTrackMetaData().getTimescale() / 2) + 1; + if (currentFragment >= fragments.length) { + break; + } + fragments[currentFragment] = samples++ + 1; + time += entry.getDelta(); + } + } + long last = samples + 1; + // fill all -1 ones. + for (int i = fragments.length - 1; i >= 0; i--) { + if (fragments[i] == -1) { + fragments[i] = last ; + } + last = fragments[i]; + } + return fragments; + + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/all-wcprops new file mode 100644 index 0000000..a7245be --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/all-wcprops @@ -0,0 +1,5 @@ +K 25 +svn:wc:ra_dav:version-url +V 92 +/svn/!svn/ver/418/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/entries new file mode 100644 index 0000000..bde4d0e --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/.svn/entries @@ -0,0 +1,31 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container +http://mp4parser.googlecode.com/svn + + + +2012-03-11T20:54:45.638478Z +418 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +mp4 +dir + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/all-wcprops new file mode 100644 index 0000000..ec2a4f9 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/all-wcprops @@ -0,0 +1,11 @@ +K 25 +svn:wc:ra_dav:version-url +V 96 +/svn/!svn/ver/418/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4 +END +MovieCreator.java +K 25 +svn:wc:ra_dav:version-url +V 114 +/svn/!svn/ver/418/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/MovieCreator.java +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/entries new file mode 100644 index 0000000..d7d7f30 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/entries @@ -0,0 +1,62 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4 +http://mp4parser.googlecode.com/svn + + + +2012-03-11T20:54:45.638478Z +418 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +MovieCreator.java +file + + + + +2012-09-14T17:27:49.987212Z +ecb22de8d79473683de67e40f494641d +2012-03-11T20:54:45.638478Z +418 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +1404 + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/text-base/MovieCreator.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/text-base/MovieCreator.java.svn-base new file mode 100644 index 0000000..ed9d15f --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/.svn/text-base/MovieCreator.java.svn-base @@ -0,0 +1,40 @@ +/* + * 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.container.mp4; + +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.boxes.TrackBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Mp4TrackImpl; + +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.util.List; + +/** + * Shortcut to build a movie from an MP4 file. + */ +public class MovieCreator { + public static Movie build(ReadableByteChannel channel) throws IOException { + IsoFile isoFile = new IsoFile(channel); + Movie m = new Movie(); + List<TrackBox> trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class); + for (TrackBox trackBox : trackBoxes) { + m.addTrack(new Mp4TrackImpl(trackBox)); + } + return m; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/MovieCreator.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/MovieCreator.java new file mode 100644 index 0000000..ed9d15f --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/container/mp4/MovieCreator.java @@ -0,0 +1,40 @@ +/* + * 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.container.mp4; + +import com.coremedia.iso.IsoFile; +import com.coremedia.iso.boxes.TrackBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Mp4TrackImpl; + +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.util.List; + +/** + * Shortcut to build a movie from an MP4 file. + */ +public class MovieCreator { + public static Movie build(ReadableByteChannel channel) throws IOException { + IsoFile isoFile = new IsoFile(channel); + Movie m = new Movie(); + List<TrackBox> trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class); + for (TrackBox trackBox : trackBoxes) { + m.addTrack(new Mp4TrackImpl(trackBox)); + } + return m; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/all-wcprops b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/all-wcprops new file mode 100644 index 0000000..496d7bb --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/all-wcprops @@ -0,0 +1,89 @@ +K 25 +svn:wc:ra_dav:version-url +V 89 +/svn/!svn/ver/756/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks +END +DivideTimeScaleTrack.java +K 25 +svn:wc:ra_dav:version-url +V 115 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/DivideTimeScaleTrack.java +END +CroppedTrack.java +K 25 +svn:wc:ra_dav:version-url +V 107 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/CroppedTrack.java +END +ChangeTimeScaleTrack.java +K 25 +svn:wc:ra_dav:version-url +V 115 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ChangeTimeScaleTrack.java +END +EC3TrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 107 +/svn/!svn/ver/756/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/EC3TrackImpl.java +END +ReplaceSampleTrack.java +K 25 +svn:wc:ra_dav:version-url +V 113 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ReplaceSampleTrack.java +END +QuicktimeTextTrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 117 +/svn/!svn/ver/691/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/QuicktimeTextTrackImpl.java +END +Amf0Track.java +K 25 +svn:wc:ra_dav:version-url +V 104 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/Amf0Track.java +END +SilenceTrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 111 +/svn/!svn/ver/698/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/SilenceTrackImpl.java +END +H264TrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 108 +/svn/!svn/ver/756/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/H264TrackImpl.java +END +TextTrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 108 +/svn/!svn/ver/684/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/TextTrackImpl.java +END +AACTrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 107 +/svn/!svn/ver/756/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AACTrackImpl.java +END +MultiplyTimeScaleTrack.java +K 25 +svn:wc:ra_dav:version-url +V 117 +/svn/!svn/ver/686/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/MultiplyTimeScaleTrack.java +END +AppendTrack.java +K 25 +svn:wc:ra_dav:version-url +V 106 +/svn/!svn/ver/714/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AppendTrack.java +END +AC3TrackImpl.java +K 25 +svn:wc:ra_dav:version-url +V 107 +/svn/!svn/ver/756/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AC3TrackImpl.java +END diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/entries b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/entries new file mode 100644 index 0000000..dbe8ae3 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/entries @@ -0,0 +1,504 @@ +10 + +dir +778 +http://mp4parser.googlecode.com/svn/trunk/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks +http://mp4parser.googlecode.com/svn + + + +2012-08-17T01:19:11.953078Z +756 +michael.stattmann@gmail.com + + + + + + + + + + + + + + +7decde4b-c250-0410-a0da-51896bc88be6 + +DivideTimeScaleTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +6aa41cdb7489e16e879ddebd68b7ac52 +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +3893 + +CroppedTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +5d9c65d9aac52a26372aa3fdf6f1b7ea +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +6025 + +ChangeTimeScaleTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +ba01d30ac4b7c4fa9a2c6538ee537594 +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +7152 + +EC3TrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +1d568bfaa0f41c3771986a7790347072 +2012-08-17T01:19:11.953078Z +756 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +13623 + +ReplaceSampleTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +f6846b7a262ab0e530da46f4d7e34850 +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +3139 + +QuicktimeTextTrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +7ccd01a58545fb02b507b57892eb53e5 +2012-06-24T21:35:59.546504Z +691 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +5128 + +Amf0Track.java +file + + + + +2012-09-14T17:27:50.507219Z +4718a34bc271adf4517de9829ac74a9d +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +3858 + +SilenceTrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +a897677e602dfa0d64af1b0d33a04ca8 +2012-06-26T08:37:32.910396Z +698 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +2623 + +H264TrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +76abb4b21c13d11215a2ac4cb0c7e461 +2012-08-17T01:19:11.953078Z +756 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +28816 + +TextTrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +5a643c876754eb5c7bcf75b4b71114a1 +2012-06-24T14:45:45.932648Z +684 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +4963 + +AACTrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +ece8d364a9a5aeabf6a281f6d428e3cf +2012-08-17T01:19:11.953078Z +756 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +10097 + +MultiplyTimeScaleTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +b0ea53239c124607a26a181f594f82a1 +2012-06-24T19:52:05.961412Z +686 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +4192 + +AppendTrack.java +file + + + + +2012-09-14T17:27:50.507219Z +e7814aebc4500724771fd0582455a7ca +2012-07-18T23:22:45.506793Z +714 +Sebastian.Annies@gmail.com + + + + + + + + + + + + + + + + + + + + + +15783 + +AC3TrackImpl.java +file + + + + +2012-09-14T17:27:50.507219Z +fbd724739cd9a5f2b28f16b412b27309 +2012-08-17T01:19:11.953078Z +756 +michael.stattmann@gmail.com + + + + + + + + + + + + + + + + + + + + + +19303 + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AACTrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AACTrackImpl.java.svn-base new file mode 100644 index 0000000..df51a1a --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AACTrackImpl.java.svn-base @@ -0,0 +1,292 @@ +/* + * Copyright 2012 castLabs GmbH, Berlin + * + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.AC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.*; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.*; + +/** + */ +public class AACTrackImpl extends AbstractTrack { + public static Map<Integer, Integer> samplingFrequencyIndexMap = new HashMap<Integer, Integer>(); + + static { + samplingFrequencyIndexMap.put(96000, 0); + samplingFrequencyIndexMap.put(88200, 1); + samplingFrequencyIndexMap.put(64000, 2); + samplingFrequencyIndexMap.put(48000, 3); + samplingFrequencyIndexMap.put(44100, 4); + samplingFrequencyIndexMap.put(32000, 5); + samplingFrequencyIndexMap.put(24000, 6); + samplingFrequencyIndexMap.put(22050, 7); + samplingFrequencyIndexMap.put(16000, 8); + samplingFrequencyIndexMap.put(12000, 9); + samplingFrequencyIndexMap.put(11025, 10); + samplingFrequencyIndexMap.put(8000, 11); + samplingFrequencyIndexMap.put(0x0, 96000); + samplingFrequencyIndexMap.put(0x1, 88200); + samplingFrequencyIndexMap.put(0x2, 64000); + samplingFrequencyIndexMap.put(0x3, 48000); + samplingFrequencyIndexMap.put(0x4, 44100); + samplingFrequencyIndexMap.put(0x5, 32000); + samplingFrequencyIndexMap.put(0x6, 24000); + samplingFrequencyIndexMap.put(0x7, 22050); + samplingFrequencyIndexMap.put(0x8, 16000); + samplingFrequencyIndexMap.put(0x9, 12000); + samplingFrequencyIndexMap.put(0xa, 11025); + samplingFrequencyIndexMap.put(0xb, 8000); + } + + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int channelCount; + int channelconfig; + + int bufferSizeDB; + long maxBitRate; + long avgBitRate; + + private BufferedInputStream inputStream; + private List<ByteBuffer> samples; + boolean readSamples = false; + List<TimeToSampleBox.Entry> stts; + private String lang = "und"; + + + public AACTrackImpl(InputStream inputStream, String lang) throws IOException { + this.lang = lang; + parse(inputStream); + } + + public AACTrackImpl(InputStream inputStream) throws IOException { + parse(inputStream); + } + + private void parse(InputStream inputStream) throws IOException { + this.inputStream = new BufferedInputStream(inputStream); + stts = new LinkedList<TimeToSampleBox.Entry>(); + + if (!readVariables()) { + throw new IOException(); + } + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + + double packetsPerSecond = (double)samplerate / 1024.0; + double duration = samples.size() / packetsPerSecond; + + long dataSize = 0; + LinkedList<Integer> queue = new LinkedList<Integer>(); + for (int i = 0; i < samples.size(); i++) { + int size = samples.get(i).capacity(); + dataSize += size; + queue.add(size); + while (queue.size() > packetsPerSecond) { + queue.pop(); + } + if (queue.size() == (int) packetsPerSecond) { + int currSize = 0; + for (int j = 0 ; j < queue.size(); j++) { + currSize += queue.get(j); + } + double currBitrate = 8.0 * currSize / queue.size() * packetsPerSecond; + if (currBitrate > maxBitRate) { + maxBitRate = (int)currBitrate; + } + } + } + + avgBitRate = (int) (8 * dataSize / duration); + + bufferSizeDB = 1536; /* TODO: Calcultate this somehow! */ + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a"); + audioSampleEntry.setChannelCount(2); + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + + ESDescriptorBox esds = new ESDescriptorBox(); + ESDescriptor descriptor = new ESDescriptor(); + descriptor.setEsId(0); + + SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor(); + slConfigDescriptor.setPredefined(2); + descriptor.setSlConfigDescriptor(slConfigDescriptor); + + DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor(); + decoderConfigDescriptor.setObjectTypeIndication(0x40); + decoderConfigDescriptor.setStreamType(5); + decoderConfigDescriptor.setBufferSizeDB(bufferSizeDB); + decoderConfigDescriptor.setMaxBitRate(maxBitRate); + decoderConfigDescriptor.setAvgBitRate(avgBitRate); + + AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig(); + audioSpecificConfig.setAudioObjectType(2); // AAC LC + audioSpecificConfig.setSamplingFrequencyIndex(samplingFrequencyIndexMap.get(samplerate)); + audioSpecificConfig.setChannelConfiguration(channelconfig); + decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig); + + descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor); + + ByteBuffer data = descriptor.serialize(); + esds.setData(data); + audioSampleEntry.addBox(esds); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public Box getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() throws IOException { + byte[] data = new byte[100]; + inputStream.mark(100); + if (100 != inputStream.read(data, 0, 100)) { + return false; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(12); + if (syncword != 0xfff) { + return false; + } + int id = brb.readBits(1); + int layer = brb.readBits(2); + int protectionAbsent = brb.readBits(1); + int profile = brb.readBits(2); + samplerate = samplingFrequencyIndexMap.get(brb.readBits(4)); + brb.readBits(1); + channelconfig = brb.readBits(3); + int original = brb.readBits(1); + int home = brb.readBits(1); + int emphasis = brb.readBits(2); + + return true; + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + + readSamples = true; + byte[] header = new byte[15]; + boolean ret = false; + inputStream.mark(15); + while (-1 != inputStream.read(header)) { + ret = true; + ByteBuffer bb = ByteBuffer.wrap(header); + inputStream.reset(); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(12); + if (syncword != 0xfff) { + return false; + } + brb.readBits(3); + int protectionAbsent = brb.readBits(1); + brb.readBits(14); + int frameSize = brb.readBits(13); + int bufferFullness = brb.readBits(11); + int noBlocks = brb.readBits(2); + int used = (int) Math.ceil(brb.getPosition() / 8.0); + if (protectionAbsent == 0) { + used += 2; + } + inputStream.skip(used); + frameSize -= used; +// System.out.println("Size: " + frameSize + " fullness: " + bufferFullness + " no blocks: " + noBlocks); + byte[] data = new byte[frameSize]; + inputStream.read(data); + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1024)); + inputStream.mark(15); + } + return ret; + } + + @Override + public String toString() { + return "AACTrackImpl{" + + "samplerate=" + samplerate + + ", bitrate=" + bitrate + + ", channelCount=" + channelCount + + ", channelconfig=" + channelconfig + + '}'; + } +} + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AC3TrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AC3TrackImpl.java.svn-base new file mode 100644 index 0000000..5e5b2cd --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AC3TrackImpl.java.svn-base @@ -0,0 +1,513 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.AC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.BitReaderBuffer; + +import java.io.InputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +public class AC3TrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int channelCount; + + int fscod; + int bsid; + int bsmod; + int acmod; + int lfeon; + int frmsizecod; + + int frameSize; + int[][][][] bitRateAndFrameSizeTable; + + private InputStream inputStream; + private List<ByteBuffer> samples; + boolean readSamples = false; + List<TimeToSampleBox.Entry> stts; + private String lang = "und"; + + public AC3TrackImpl(InputStream fin, String lang) throws IOException { + this.lang = lang; + parse(fin); + } + + public AC3TrackImpl(InputStream fin) throws IOException { + parse(fin); + } + + private void parse(InputStream fin) throws IOException { + inputStream = fin; + bitRateAndFrameSizeTable = new int[19][2][3][2]; + stts = new LinkedList<TimeToSampleBox.Entry>(); + initBitRateAndFrameSizeTable(); + if (!readVariables()) { + throw new IOException(); + } + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("ac-3"); + audioSampleEntry.setChannelCount(2); // According to ETSI TS 102 366 Annex F + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + AC3SpecificBox ac3 = new AC3SpecificBox(); + ac3.setAcmod(acmod); + ac3.setBitRateCode(frmsizecod >> 1); + ac3.setBsid(bsid); + ac3.setBsmod(bsmod); + ac3.setFscod(fscod); + ac3.setLfeon(lfeon); + ac3.setReserved(0); + + audioSampleEntry.addBox(ac3); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + } + + + public List<ByteBuffer> getSamples() { + + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public Box getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() throws IOException { + byte[] data = new byte[100]; + inputStream.mark(100); + if (100 != inputStream.read(data, 0, 100)) { + return false; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(16); + if (syncword != 0xb77) { + return false; + } + brb.readBits(16); // CRC-1 + fscod = brb.readBits(2); + + switch (fscod) { + case 0: + samplerate = 48000; + break; + + case 1: + samplerate = 44100; + break; + + case 2: + samplerate = 32000; + break; + + case 3: + samplerate = 0; + break; + + } + if (samplerate == 0) { + return false; + } + + frmsizecod = brb.readBits(6); + + if (!calcBitrateAndFrameSize(frmsizecod)) { + return false; + } + + if (frameSize == 0) { + return false; + } + bsid = brb.readBits(5); + bsmod = brb.readBits(3); + acmod = brb.readBits(3); + + if (bsid == 9) { + samplerate /= 2; + } else if (bsid != 8 && bsid != 6) { + return false; + } + + if ((acmod != 1) && ((acmod & 1) == 1)) { + brb.readBits(2); + } + + if (0 != (acmod & 4)) { + brb.readBits(2); + } + + if (acmod == 2) { + brb.readBits(2); + } + + switch (acmod) { + case 0: + channelCount = 2; + break; + + case 1: + channelCount = 1; + break; + + case 2: + channelCount = 2; + break; + + case 3: + channelCount = 3; + break; + + case 4: + channelCount = 3; + break; + + case 5: + channelCount = 4; + break; + + case 6: + channelCount = 4; + break; + + case 7: + channelCount = 5; + break; + + } + + lfeon = brb.readBits(1); + + if (lfeon == 1) { + channelCount++; + } + return true; + } + + private boolean calcBitrateAndFrameSize(int code) { + int frmsizecode = code >>> 1; + int flag = code & 1; + if (frmsizecode > 18 || flag > 1 || fscod > 2) { + return false; + } + bitrate = bitRateAndFrameSizeTable[frmsizecode][flag][fscod][0]; + frameSize = 2 * bitRateAndFrameSizeTable[frmsizecode][flag][fscod][1]; + return true; + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + readSamples = true; + byte[] header = new byte[5]; + boolean ret = false; + inputStream.mark(5); + while (-1 != inputStream.read(header)) { + ret = true; + int frmsizecode = header[4] & 63; + calcBitrateAndFrameSize(frmsizecode); + inputStream.reset(); + byte[] data = new byte[frameSize]; + inputStream.read(data); + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1536)); + inputStream.mark(5); + } + return ret; + } + + private void initBitRateAndFrameSizeTable() { + // ETSI 102 366 Table 4.13, in frmsizecod, flag, fscod, bitrate/size order. Note that all sizes are in words, and all bitrates in kbps + + // 48kHz + bitRateAndFrameSizeTable[0][0][0][0] = 32; + bitRateAndFrameSizeTable[0][1][0][0] = 32; + bitRateAndFrameSizeTable[0][0][0][1] = 64; + bitRateAndFrameSizeTable[0][1][0][1] = 64; + bitRateAndFrameSizeTable[1][0][0][0] = 40; + bitRateAndFrameSizeTable[1][1][0][0] = 40; + bitRateAndFrameSizeTable[1][0][0][1] = 80; + bitRateAndFrameSizeTable[1][1][0][1] = 80; + bitRateAndFrameSizeTable[2][0][0][0] = 48; + bitRateAndFrameSizeTable[2][1][0][0] = 48; + bitRateAndFrameSizeTable[2][0][0][1] = 96; + bitRateAndFrameSizeTable[2][1][0][1] = 96; + bitRateAndFrameSizeTable[3][0][0][0] = 56; + bitRateAndFrameSizeTable[3][1][0][0] = 56; + bitRateAndFrameSizeTable[3][0][0][1] = 112; + bitRateAndFrameSizeTable[3][1][0][1] = 112; + bitRateAndFrameSizeTable[4][0][0][0] = 64; + bitRateAndFrameSizeTable[4][1][0][0] = 64; + bitRateAndFrameSizeTable[4][0][0][1] = 128; + bitRateAndFrameSizeTable[4][1][0][1] = 128; + bitRateAndFrameSizeTable[5][0][0][0] = 80; + bitRateAndFrameSizeTable[5][1][0][0] = 80; + bitRateAndFrameSizeTable[5][0][0][1] = 160; + bitRateAndFrameSizeTable[5][1][0][1] = 160; + bitRateAndFrameSizeTable[6][0][0][0] = 96; + bitRateAndFrameSizeTable[6][1][0][0] = 96; + bitRateAndFrameSizeTable[6][0][0][1] = 192; + bitRateAndFrameSizeTable[6][1][0][1] = 192; + bitRateAndFrameSizeTable[7][0][0][0] = 112; + bitRateAndFrameSizeTable[7][1][0][0] = 112; + bitRateAndFrameSizeTable[7][0][0][1] = 224; + bitRateAndFrameSizeTable[7][1][0][1] = 224; + bitRateAndFrameSizeTable[8][0][0][0] = 128; + bitRateAndFrameSizeTable[8][1][0][0] = 128; + bitRateAndFrameSizeTable[8][0][0][1] = 256; + bitRateAndFrameSizeTable[8][1][0][1] = 256; + bitRateAndFrameSizeTable[9][0][0][0] = 160; + bitRateAndFrameSizeTable[9][1][0][0] = 160; + bitRateAndFrameSizeTable[9][0][0][1] = 320; + bitRateAndFrameSizeTable[9][1][0][1] = 320; + bitRateAndFrameSizeTable[10][0][0][0] = 192; + bitRateAndFrameSizeTable[10][1][0][0] = 192; + bitRateAndFrameSizeTable[10][0][0][1] = 384; + bitRateAndFrameSizeTable[10][1][0][1] = 384; + bitRateAndFrameSizeTable[11][0][0][0] = 224; + bitRateAndFrameSizeTable[11][1][0][0] = 224; + bitRateAndFrameSizeTable[11][0][0][1] = 448; + bitRateAndFrameSizeTable[11][1][0][1] = 448; + bitRateAndFrameSizeTable[12][0][0][0] = 256; + bitRateAndFrameSizeTable[12][1][0][0] = 256; + bitRateAndFrameSizeTable[12][0][0][1] = 512; + bitRateAndFrameSizeTable[12][1][0][1] = 512; + bitRateAndFrameSizeTable[13][0][0][0] = 320; + bitRateAndFrameSizeTable[13][1][0][0] = 320; + bitRateAndFrameSizeTable[13][0][0][1] = 640; + bitRateAndFrameSizeTable[13][1][0][1] = 640; + bitRateAndFrameSizeTable[14][0][0][0] = 384; + bitRateAndFrameSizeTable[14][1][0][0] = 384; + bitRateAndFrameSizeTable[14][0][0][1] = 768; + bitRateAndFrameSizeTable[14][1][0][1] = 768; + bitRateAndFrameSizeTable[15][0][0][0] = 448; + bitRateAndFrameSizeTable[15][1][0][0] = 448; + bitRateAndFrameSizeTable[15][0][0][1] = 896; + bitRateAndFrameSizeTable[15][1][0][1] = 896; + bitRateAndFrameSizeTable[16][0][0][0] = 512; + bitRateAndFrameSizeTable[16][1][0][0] = 512; + bitRateAndFrameSizeTable[16][0][0][1] = 1024; + bitRateAndFrameSizeTable[16][1][0][1] = 1024; + bitRateAndFrameSizeTable[17][0][0][0] = 576; + bitRateAndFrameSizeTable[17][1][0][0] = 576; + bitRateAndFrameSizeTable[17][0][0][1] = 1152; + bitRateAndFrameSizeTable[17][1][0][1] = 1152; + bitRateAndFrameSizeTable[18][0][0][0] = 640; + bitRateAndFrameSizeTable[18][1][0][0] = 640; + bitRateAndFrameSizeTable[18][0][0][1] = 1280; + bitRateAndFrameSizeTable[18][1][0][1] = 1280; + + // 44.1 kHz + bitRateAndFrameSizeTable[0][0][1][0] = 32; + bitRateAndFrameSizeTable[0][1][1][0] = 32; + bitRateAndFrameSizeTable[0][0][1][1] = 69; + bitRateAndFrameSizeTable[0][1][1][1] = 70; + bitRateAndFrameSizeTable[1][0][1][0] = 40; + bitRateAndFrameSizeTable[1][1][1][0] = 40; + bitRateAndFrameSizeTable[1][0][1][1] = 87; + bitRateAndFrameSizeTable[1][1][1][1] = 88; + bitRateAndFrameSizeTable[2][0][1][0] = 48; + bitRateAndFrameSizeTable[2][1][1][0] = 48; + bitRateAndFrameSizeTable[2][0][1][1] = 104; + bitRateAndFrameSizeTable[2][1][1][1] = 105; + bitRateAndFrameSizeTable[3][0][1][0] = 56; + bitRateAndFrameSizeTable[3][1][1][0] = 56; + bitRateAndFrameSizeTable[3][0][1][1] = 121; + bitRateAndFrameSizeTable[3][1][1][1] = 122; + bitRateAndFrameSizeTable[4][0][1][0] = 64; + bitRateAndFrameSizeTable[4][1][1][0] = 64; + bitRateAndFrameSizeTable[4][0][1][1] = 139; + bitRateAndFrameSizeTable[4][1][1][1] = 140; + bitRateAndFrameSizeTable[5][0][1][0] = 80; + bitRateAndFrameSizeTable[5][1][1][0] = 80; + bitRateAndFrameSizeTable[5][0][1][1] = 174; + bitRateAndFrameSizeTable[5][1][1][1] = 175; + bitRateAndFrameSizeTable[6][0][1][0] = 96; + bitRateAndFrameSizeTable[6][1][1][0] = 96; + bitRateAndFrameSizeTable[6][0][1][1] = 208; + bitRateAndFrameSizeTable[6][1][1][1] = 209; + bitRateAndFrameSizeTable[7][0][1][0] = 112; + bitRateAndFrameSizeTable[7][1][1][0] = 112; + bitRateAndFrameSizeTable[7][0][1][1] = 243; + bitRateAndFrameSizeTable[7][1][1][1] = 244; + bitRateAndFrameSizeTable[8][0][1][0] = 128; + bitRateAndFrameSizeTable[8][1][1][0] = 128; + bitRateAndFrameSizeTable[8][0][1][1] = 278; + bitRateAndFrameSizeTable[8][1][1][1] = 279; + bitRateAndFrameSizeTable[9][0][1][0] = 160; + bitRateAndFrameSizeTable[9][1][1][0] = 160; + bitRateAndFrameSizeTable[9][0][1][1] = 348; + bitRateAndFrameSizeTable[9][1][1][1] = 349; + bitRateAndFrameSizeTable[10][0][1][0] = 192; + bitRateAndFrameSizeTable[10][1][1][0] = 192; + bitRateAndFrameSizeTable[10][0][1][1] = 417; + bitRateAndFrameSizeTable[10][1][1][1] = 418; + bitRateAndFrameSizeTable[11][0][1][0] = 224; + bitRateAndFrameSizeTable[11][1][1][0] = 224; + bitRateAndFrameSizeTable[11][0][1][1] = 487; + bitRateAndFrameSizeTable[11][1][1][1] = 488; + bitRateAndFrameSizeTable[12][0][1][0] = 256; + bitRateAndFrameSizeTable[12][1][1][0] = 256; + bitRateAndFrameSizeTable[12][0][1][1] = 557; + bitRateAndFrameSizeTable[12][1][1][1] = 558; + bitRateAndFrameSizeTable[13][0][1][0] = 320; + bitRateAndFrameSizeTable[13][1][1][0] = 320; + bitRateAndFrameSizeTable[13][0][1][1] = 696; + bitRateAndFrameSizeTable[13][1][1][1] = 697; + bitRateAndFrameSizeTable[14][0][1][0] = 384; + bitRateAndFrameSizeTable[14][1][1][0] = 384; + bitRateAndFrameSizeTable[14][0][1][1] = 835; + bitRateAndFrameSizeTable[14][1][1][1] = 836; + bitRateAndFrameSizeTable[15][0][1][0] = 448; + bitRateAndFrameSizeTable[15][1][1][0] = 448; + bitRateAndFrameSizeTable[15][0][1][1] = 975; + bitRateAndFrameSizeTable[15][1][1][1] = 975; + bitRateAndFrameSizeTable[16][0][1][0] = 512; + bitRateAndFrameSizeTable[16][1][1][0] = 512; + bitRateAndFrameSizeTable[16][0][1][1] = 1114; + bitRateAndFrameSizeTable[16][1][1][1] = 1115; + bitRateAndFrameSizeTable[17][0][1][0] = 576; + bitRateAndFrameSizeTable[17][1][1][0] = 576; + bitRateAndFrameSizeTable[17][0][1][1] = 1253; + bitRateAndFrameSizeTable[17][1][1][1] = 1254; + bitRateAndFrameSizeTable[18][0][1][0] = 640; + bitRateAndFrameSizeTable[18][1][1][0] = 640; + bitRateAndFrameSizeTable[18][0][1][1] = 1393; + bitRateAndFrameSizeTable[18][1][1][1] = 1394; + + // 32kHz + bitRateAndFrameSizeTable[0][0][2][0] = 32; + bitRateAndFrameSizeTable[0][1][2][0] = 32; + bitRateAndFrameSizeTable[0][0][2][1] = 96; + bitRateAndFrameSizeTable[0][1][2][1] = 96; + bitRateAndFrameSizeTable[1][0][2][0] = 40; + bitRateAndFrameSizeTable[1][1][2][0] = 40; + bitRateAndFrameSizeTable[1][0][2][1] = 120; + bitRateAndFrameSizeTable[1][1][2][1] = 120; + bitRateAndFrameSizeTable[2][0][2][0] = 48; + bitRateAndFrameSizeTable[2][1][2][0] = 48; + bitRateAndFrameSizeTable[2][0][2][1] = 144; + bitRateAndFrameSizeTable[2][1][2][1] = 144; + bitRateAndFrameSizeTable[3][0][2][0] = 56; + bitRateAndFrameSizeTable[3][1][2][0] = 56; + bitRateAndFrameSizeTable[3][0][2][1] = 168; + bitRateAndFrameSizeTable[3][1][2][1] = 168; + bitRateAndFrameSizeTable[4][0][2][0] = 64; + bitRateAndFrameSizeTable[4][1][2][0] = 64; + bitRateAndFrameSizeTable[4][0][2][1] = 192; + bitRateAndFrameSizeTable[4][1][2][1] = 192; + bitRateAndFrameSizeTable[5][0][2][0] = 80; + bitRateAndFrameSizeTable[5][1][2][0] = 80; + bitRateAndFrameSizeTable[5][0][2][1] = 240; + bitRateAndFrameSizeTable[5][1][2][1] = 240; + bitRateAndFrameSizeTable[6][0][2][0] = 96; + bitRateAndFrameSizeTable[6][1][2][0] = 96; + bitRateAndFrameSizeTable[6][0][2][1] = 288; + bitRateAndFrameSizeTable[6][1][2][1] = 288; + bitRateAndFrameSizeTable[7][0][2][0] = 112; + bitRateAndFrameSizeTable[7][1][2][0] = 112; + bitRateAndFrameSizeTable[7][0][2][1] = 336; + bitRateAndFrameSizeTable[7][1][2][1] = 336; + bitRateAndFrameSizeTable[8][0][2][0] = 128; + bitRateAndFrameSizeTable[8][1][2][0] = 128; + bitRateAndFrameSizeTable[8][0][2][1] = 384; + bitRateAndFrameSizeTable[8][1][2][1] = 384; + bitRateAndFrameSizeTable[9][0][2][0] = 160; + bitRateAndFrameSizeTable[9][1][2][0] = 160; + bitRateAndFrameSizeTable[9][0][2][1] = 480; + bitRateAndFrameSizeTable[9][1][2][1] = 480; + bitRateAndFrameSizeTable[10][0][2][0] = 192; + bitRateAndFrameSizeTable[10][1][2][0] = 192; + bitRateAndFrameSizeTable[10][0][2][1] = 576; + bitRateAndFrameSizeTable[10][1][2][1] = 576; + bitRateAndFrameSizeTable[11][0][2][0] = 224; + bitRateAndFrameSizeTable[11][1][2][0] = 224; + bitRateAndFrameSizeTable[11][0][2][1] = 672; + bitRateAndFrameSizeTable[11][1][2][1] = 672; + bitRateAndFrameSizeTable[12][0][2][0] = 256; + bitRateAndFrameSizeTable[12][1][2][0] = 256; + bitRateAndFrameSizeTable[12][0][2][1] = 768; + bitRateAndFrameSizeTable[12][1][2][1] = 768; + bitRateAndFrameSizeTable[13][0][2][0] = 320; + bitRateAndFrameSizeTable[13][1][2][0] = 320; + bitRateAndFrameSizeTable[13][0][2][1] = 960; + bitRateAndFrameSizeTable[13][1][2][1] = 960; + bitRateAndFrameSizeTable[14][0][2][0] = 384; + bitRateAndFrameSizeTable[14][1][2][0] = 384; + bitRateAndFrameSizeTable[14][0][2][1] = 1152; + bitRateAndFrameSizeTable[14][1][2][1] = 1152; + bitRateAndFrameSizeTable[15][0][2][0] = 448; + bitRateAndFrameSizeTable[15][1][2][0] = 448; + bitRateAndFrameSizeTable[15][0][2][1] = 1344; + bitRateAndFrameSizeTable[15][1][2][1] = 1344; + bitRateAndFrameSizeTable[16][0][2][0] = 512; + bitRateAndFrameSizeTable[16][1][2][0] = 512; + bitRateAndFrameSizeTable[16][0][2][1] = 1536; + bitRateAndFrameSizeTable[16][1][2][1] = 1536; + bitRateAndFrameSizeTable[17][0][2][0] = 576; + bitRateAndFrameSizeTable[17][1][2][0] = 576; + bitRateAndFrameSizeTable[17][0][2][1] = 1728; + bitRateAndFrameSizeTable[17][1][2][1] = 1728; + bitRateAndFrameSizeTable[18][0][2][0] = 640; + bitRateAndFrameSizeTable[18][1][2][0] = 640; + bitRateAndFrameSizeTable[18][0][2][1] = 1920; + bitRateAndFrameSizeTable[18][1][2][1] = 1920; + } +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/Amf0Track.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/Amf0Track.java.svn-base new file mode 100644 index 0000000..0917767 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/Amf0Track.java.svn-base @@ -0,0 +1,116 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.adobe.ActionMessageFormat0SampleEntryBox; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +public class Amf0Track extends AbstractTrack { + SortedMap<Long, byte[]> rawSamples = new TreeMap<Long, byte[]>() { + }; + private TrackMetaData trackMetaData = new TrackMetaData(); + + + /** + * Creates a new AMF0 track from + * + * @param rawSamples + */ + public Amf0Track(Map<Long, byte[]> rawSamples) { + this.rawSamples = new TreeMap<Long, byte[]>(rawSamples); + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); // Text tracks use millieseconds + trackMetaData.setLanguage("eng"); + } + + public List<ByteBuffer> getSamples() { + LinkedList<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + for (byte[] bytes : rawSamples.values()) { + samples.add(ByteBuffer.wrap(bytes)); + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + SampleDescriptionBox stsd = new SampleDescriptionBox(); + ActionMessageFormat0SampleEntryBox amf0 = new ActionMessageFormat0SampleEntryBox(); + amf0.setDataReferenceIndex(1); + stsd.addBox(amf0); + return stsd; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + LinkedList<TimeToSampleBox.Entry> timesToSample = new LinkedList<TimeToSampleBox.Entry>(); + LinkedList<Long> keys = new LinkedList<Long>(rawSamples.keySet()); + Collections.sort(keys); + long lastTimeStamp = 0; + for (Long key : keys) { + long delta = key - lastTimeStamp; + if (timesToSample.size() > 0 && timesToSample.peek().getDelta() == delta) { + timesToSample.peek().setCount(timesToSample.peek().getCount() + 1); + } else { + timesToSample.add(new TimeToSampleBox.Entry(1, delta)); + } + lastTimeStamp = key; + } + return timesToSample; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + // AMF0 tracks do not have Composition Time + return null; + } + + public long[] getSyncSamples() { + // AMF0 tracks do not have Sync Samples + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + // AMF0 tracks do not have Sample Dependencies + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; //To change body of implemented methods use File | Settings | File Templates. + } + + public String getHandler() { + return "data"; + } + + public Box getMediaHeaderBox() { + return new NullMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AppendTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AppendTrack.java.svn-base new file mode 100644 index 0000000..93ee0cd --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/AppendTrack.java.svn-base @@ -0,0 +1,348 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.*; + +/** + * Appends two or more <code>Tracks</code> of the same type. No only that the type must be equal + * also the decoder settings must be the same. + */ +public class AppendTrack extends AbstractTrack { + Track[] tracks; + SampleDescriptionBox stsd; + + public AppendTrack(Track... tracks) throws IOException { + this.tracks = tracks; + + for (Track track : tracks) { + + if (stsd == null) { + stsd = track.getSampleDescriptionBox(); + } else { + ByteArrayOutputStream curBaos = new ByteArrayOutputStream(); + ByteArrayOutputStream refBaos = new ByteArrayOutputStream(); + track.getSampleDescriptionBox().getBox(Channels.newChannel(curBaos)); + stsd.getBox(Channels.newChannel(refBaos)); + byte[] cur = curBaos.toByteArray(); + byte[] ref = refBaos.toByteArray(); + + if (!Arrays.equals(ref, cur)) { + SampleDescriptionBox curStsd = track.getSampleDescriptionBox(); + if (stsd.getBoxes().size() == 1 && curStsd.getBoxes().size() == 1) { + if (stsd.getBoxes().get(0) instanceof AudioSampleEntry && curStsd.getBoxes().get(0) instanceof AudioSampleEntry) { + AudioSampleEntry aseResult = mergeAudioSampleEntries((AudioSampleEntry) stsd.getBoxes().get(0), (AudioSampleEntry) curStsd.getBoxes().get(0)); + if (aseResult != null) { + stsd.setBoxes(Collections.<Box>singletonList(aseResult)); + return; + } + } + } + throw new IOException("Cannot append " + track + " to " + tracks[0] + " since their Sample Description Boxes differ: \n" + track.getSampleDescriptionBox() + "\n vs. \n" + tracks[0].getSampleDescriptionBox()); + } + } + } + } + + private AudioSampleEntry mergeAudioSampleEntries(AudioSampleEntry ase1, AudioSampleEntry ase2) throws IOException { + if (ase1.getType().equals(ase2.getType())) { + AudioSampleEntry ase = new AudioSampleEntry(ase2.getType()); + if (ase1.getBytesPerFrame() == ase2.getBytesPerFrame()) { + ase.setBytesPerFrame(ase1.getBytesPerFrame()); + } else { + return null; + } + if (ase1.getBytesPerPacket() == ase2.getBytesPerPacket()) { + ase.setBytesPerPacket(ase1.getBytesPerPacket()); + } else { + return null; + } + if (ase1.getBytesPerSample() == ase2.getBytesPerSample()) { + ase.setBytesPerSample(ase1.getBytesPerSample()); + } else { + return null; + } + if (ase1.getChannelCount() == ase2.getChannelCount()) { + ase.setChannelCount(ase1.getChannelCount()); + } else { + return null; + } + if (ase1.getPacketSize() == ase2.getPacketSize()) { + ase.setPacketSize(ase1.getPacketSize()); + } else { + return null; + } + if (ase1.getCompressionId() == ase2.getCompressionId()) { + ase.setCompressionId(ase1.getCompressionId()); + } else { + return null; + } + if (ase1.getSampleRate() == ase2.getSampleRate()) { + ase.setSampleRate(ase1.getSampleRate()); + } else { + return null; + } + if (ase1.getSampleSize() == ase2.getSampleSize()) { + ase.setSampleSize(ase1.getSampleSize()); + } else { + return null; + } + if (ase1.getSamplesPerPacket() == ase2.getSamplesPerPacket()) { + ase.setSamplesPerPacket(ase1.getSamplesPerPacket()); + } else { + return null; + } + if (ase1.getSoundVersion() == ase2.getSoundVersion()) { + ase.setSoundVersion(ase1.getSoundVersion()); + } else { + return null; + } + if (Arrays.equals(ase1.getSoundVersion2Data(), ase2.getSoundVersion2Data())) { + ase.setSoundVersion2Data(ase1.getSoundVersion2Data()); + } else { + return null; + } + if (ase1.getBoxes().size() == ase2.getBoxes().size()) { + Iterator<Box> bxs1 = ase1.getBoxes().iterator(); + Iterator<Box> bxs2 = ase2.getBoxes().iterator(); + while (bxs1.hasNext()) { + Box cur1 = bxs1.next(); + Box cur2 = bxs2.next(); + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + cur1.getBox(Channels.newChannel(baos1)); + cur2.getBox(Channels.newChannel(baos2)); + if (Arrays.equals(baos1.toByteArray(), baos2.toByteArray())) { + ase.addBox(cur1); + } else { + if (ESDescriptorBox.TYPE.equals(cur1.getType()) && ESDescriptorBox.TYPE.equals(cur2.getType())) { + ESDescriptorBox esdsBox1 = (ESDescriptorBox) cur1; + ESDescriptorBox esdsBox2 = (ESDescriptorBox) cur2; + ESDescriptor esds1 = esdsBox1.getEsDescriptor(); + ESDescriptor esds2 = esdsBox2.getEsDescriptor(); + if (esds1.getURLFlag() != esds2.getURLFlag()) { + return null; + } + if (esds1.getURLLength() != esds2.getURLLength()) { + return null; + } + if (esds1.getDependsOnEsId() != esds2.getDependsOnEsId()) { + return null; + } + if (esds1.getEsId() != esds2.getEsId()) { + return null; + } + if (esds1.getoCREsId() != esds2.getoCREsId()) { + return null; + } + if (esds1.getoCRstreamFlag() != esds2.getoCRstreamFlag()) { + return null; + } + if (esds1.getRemoteODFlag() != esds2.getRemoteODFlag()) { + return null; + } + if (esds1.getStreamDependenceFlag() != esds2.getStreamDependenceFlag()) { + return null; + } + if (esds1.getStreamPriority() != esds2.getStreamPriority()) { + return null; + } + if (esds1.getURLString() != null ? !esds1.getURLString().equals(esds2.getURLString()) : esds2.getURLString() != null) { + return null; + } + if (esds1.getDecoderConfigDescriptor() != null ? !esds1.getDecoderConfigDescriptor().equals(esds2.getDecoderConfigDescriptor()) : esds2.getDecoderConfigDescriptor() != null) { + DecoderConfigDescriptor dcd1 = esds1.getDecoderConfigDescriptor(); + DecoderConfigDescriptor dcd2 = esds2.getDecoderConfigDescriptor(); + if (!dcd1.getAudioSpecificInfo().equals(dcd2.getAudioSpecificInfo())) { + return null; + } + if (dcd1.getAvgBitRate() != dcd2.getAvgBitRate()) { + // I don't care + } + if (dcd1.getBufferSizeDB() != dcd2.getBufferSizeDB()) { + // I don't care + } + + if (dcd1.getDecoderSpecificInfo() != null ? !dcd1.getDecoderSpecificInfo().equals(dcd2.getDecoderSpecificInfo()) : dcd2.getDecoderSpecificInfo() != null) { + return null; + } + + if (dcd1.getMaxBitRate() != dcd2.getMaxBitRate()) { + // I don't care + } + if (!dcd1.getProfileLevelIndicationDescriptors().equals(dcd2.getProfileLevelIndicationDescriptors())) { + return null; + } + + if (dcd1.getObjectTypeIndication() != dcd2.getObjectTypeIndication()) { + return null; + } + if (dcd1.getStreamType() != dcd2.getStreamType()) { + return null; + } + if (dcd1.getUpStream() != dcd2.getUpStream()) { + return null; + } + + + } + if (esds1.getOtherDescriptors() != null ? !esds1.getOtherDescriptors().equals(esds2.getOtherDescriptors()) : esds2.getOtherDescriptors() != null) { + return null; + } + if (esds1.getSlConfigDescriptor() != null ? !esds1.getSlConfigDescriptor().equals(esds2.getSlConfigDescriptor()) : esds2.getSlConfigDescriptor() != null) { + return null; + } + ase.addBox(cur1); + } + } + } + } + return ase; + } else { + return null; + } + + + } + + + public List<ByteBuffer> getSamples() { + ArrayList<ByteBuffer> lists = new ArrayList<ByteBuffer>(); + + for (Track track : tracks) { + lists.addAll(track.getSamples()); + } + + return lists; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return stsd; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + if (tracks[0].getDecodingTimeEntries() != null && !tracks[0].getDecodingTimeEntries().isEmpty()) { + List<long[]> lists = new LinkedList<long[]>(); + for (Track track : tracks) { + lists.add(TimeToSampleBox.blowupTimeToSamples(track.getDecodingTimeEntries())); + } + + LinkedList<TimeToSampleBox.Entry> returnDecodingEntries = new LinkedList<TimeToSampleBox.Entry>(); + for (long[] list : lists) { + for (long nuDecodingTime : list) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getDelta() != nuDecodingTime) { + TimeToSampleBox.Entry e = new TimeToSampleBox.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + TimeToSampleBox.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + if (tracks[0].getCompositionTimeEntries() != null && !tracks[0].getCompositionTimeEntries().isEmpty()) { + List<int[]> lists = new LinkedList<int[]>(); + for (Track track : tracks) { + lists.add(CompositionTimeToSample.blowupCompositionTimes(track.getCompositionTimeEntries())); + } + LinkedList<CompositionTimeToSample.Entry> compositionTimeEntries = new LinkedList<CompositionTimeToSample.Entry>(); + for (int[] list : lists) { + for (int compositionTime : list) { + if (compositionTimeEntries.isEmpty() || compositionTimeEntries.getLast().getOffset() != compositionTime) { + CompositionTimeToSample.Entry e = new CompositionTimeToSample.Entry(1, compositionTime); + compositionTimeEntries.add(e); + } else { + CompositionTimeToSample.Entry e = compositionTimeEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + } + return compositionTimeEntries; + } else { + return null; + } + } + + public long[] getSyncSamples() { + if (tracks[0].getSyncSamples() != null && tracks[0].getSyncSamples().length > 0) { + int numSyncSamples = 0; + for (Track track : tracks) { + numSyncSamples += track.getSyncSamples().length; + } + long[] returnSyncSamples = new long[numSyncSamples]; + + int pos = 0; + long samplesBefore = 0; + for (Track track : tracks) { + for (long l : track.getSyncSamples()) { + returnSyncSamples[pos++] = samplesBefore + l; + } + samplesBefore += track.getSamples().size(); + } + return returnSyncSamples; + } else { + return null; + } + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + if (tracks[0].getSampleDependencies() != null && !tracks[0].getSampleDependencies().isEmpty()) { + List<SampleDependencyTypeBox.Entry> list = new LinkedList<SampleDependencyTypeBox.Entry>(); + for (Track track : tracks) { + list.addAll(track.getSampleDependencies()); + } + return list; + } else { + return null; + } + } + + public TrackMetaData getTrackMetaData() { + return tracks[0].getTrackMetaData(); + } + + public String getHandler() { + return tracks[0].getHandler(); + } + + public Box getMediaHeaderBox() { + return tracks[0].getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return tracks[0].getSubsampleInformationBox(); + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ChangeTimeScaleTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ChangeTimeScaleTrack.java.svn-base new file mode 100644 index 0000000..50f76c2 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ChangeTimeScaleTrack.java.svn-base @@ -0,0 +1,203 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.logging.Logger; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class ChangeTimeScaleTrack implements Track { + private static final Logger LOG = Logger.getLogger(ChangeTimeScaleTrack.class.getName()); + + Track source; + List<CompositionTimeToSample.Entry> ctts; + List<TimeToSampleBox.Entry> tts; + long timeScale; + + /** + * Changes the time scale of the source track to the target time scale and makes sure + * that any rounding errors that may have summed are corrected exactly before the syncSamples. + * + * @param source the source track + * @param targetTimeScale the resulting time scale of this track. + * @param syncSamples at these sync points where rounding error are corrected. + */ + public ChangeTimeScaleTrack(Track source, long targetTimeScale, long[] syncSamples) { + this.source = source; + this.timeScale = targetTimeScale; + double timeScaleFactor = (double) targetTimeScale / source.getTrackMetaData().getTimescale(); + ctts = adjustCtts(source.getCompositionTimeEntries(), timeScaleFactor); + tts = adjustTts(source.getDecodingTimeEntries(), timeScaleFactor, syncSamples, getTimes(source, syncSamples, targetTimeScale)); + } + + private static long[] getTimes(Track track, long[] syncSamples, long targetTimeScale) { + long[] syncSampleTimes = new long[syncSamples.length]; + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + + int currentSample = 1; // first syncsample is 1 + long currentDuration = 0; + long currentDelta = 0; + int currentSyncSampleIndex = 0; + long left = 0; + + + while (currentSample <= syncSamples[syncSamples.length - 1]) { + if (currentSample++ == syncSamples[currentSyncSampleIndex]) { + syncSampleTimes[currentSyncSampleIndex++] = (currentDuration * targetTimeScale) / track.getTrackMetaData().getTimescale(); + } + if (left-- == 0) { + TimeToSampleBox.Entry entry = timeQueue.poll(); + left = entry.getCount() - 1; + currentDelta = entry.getDelta(); + } + currentDuration += currentDelta; + } + return syncSampleTimes; + + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return tts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return ctts; + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(timeScale); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + /** + * Adjusting the composition times is easy. Just scale it by the factor - that's it. There is no rounding + * error summing up. + * + * @param source + * @param timeScaleFactor + * @return + */ + static List<CompositionTimeToSample.Entry> adjustCtts(List<CompositionTimeToSample.Entry> source, double timeScaleFactor) { + if (source != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(source.size()); + for (CompositionTimeToSample.Entry entry : source) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), (int) Math.round(timeScaleFactor * entry.getOffset()))); + } + return entries2; + } else { + return null; + } + } + + static List<TimeToSampleBox.Entry> adjustTts(List<TimeToSampleBox.Entry> source, double timeScaleFactor, long[] syncSample, long[] syncSampleTimes) { + + long[] sourceArray = TimeToSampleBox.blowupTimeToSamples(source); + long summedDurations = 0; + + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (int i = 1; i <= sourceArray.length; i++) { + long duration = sourceArray[i - 1]; + + long x = Math.round(timeScaleFactor * duration); + + + TimeToSampleBox.Entry last = entries2.peekLast(); + int ssIndex; + if ((ssIndex = Arrays.binarySearch(syncSample, i + 1)) >= 0) { + // we are at the sample before sync point + if (syncSampleTimes[ssIndex] != summedDurations) { + long correction = syncSampleTimes[ssIndex] - (summedDurations + x); + LOG.finest(String.format("Sample %d %d / %d - correct by %d", i, summedDurations, syncSampleTimes[ssIndex], correction)); + x += correction; + } + } + summedDurations += x; + if (last == null) { + entries2.add(new TimeToSampleBox.Entry(1, x)); + } else if (last.getDelta() != x) { + entries2.add(new TimeToSampleBox.Entry(1, x)); + } else { + last.setCount(last.getCount() + 1); + } + + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "ChangeTimeScaleTrack{" + + "source=" + source + + '}'; + } +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/CroppedTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/CroppedTrack.java.svn-base new file mode 100644 index 0000000..2389961 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/CroppedTrack.java.svn-base @@ -0,0 +1,151 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +/** + * Generates a Track that starts at fromSample and ends at toSample (exclusive). The user of this class + * has to make sure that the fromSample is a random access sample. + * <ul> + * <li>In AAC this is every single sample</li> + * <li>In H264 this is every sample that is marked in the SyncSampleBox</li> + * </ul> + */ +public class CroppedTrack extends AbstractTrack { + Track origTrack; + private int fromSample; + private int toSample; + private long[] syncSampleArray; + + public CroppedTrack(Track origTrack, long fromSample, long toSample) { + this.origTrack = origTrack; + assert fromSample <= Integer.MAX_VALUE; + assert toSample <= Integer.MAX_VALUE; + this.fromSample = (int) fromSample; + this.toSample = (int) toSample; + } + + public List<ByteBuffer> getSamples() { + return origTrack.getSamples().subList(fromSample, toSample); + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return origTrack.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + if (origTrack.getDecodingTimeEntries() != null && !origTrack.getDecodingTimeEntries().isEmpty()) { + // todo optimize! too much long is allocated but then not used + long[] decodingTimes = TimeToSampleBox.blowupTimeToSamples(origTrack.getDecodingTimeEntries()); + long[] nuDecodingTimes = new long[toSample - fromSample]; + System.arraycopy(decodingTimes, fromSample, nuDecodingTimes, 0, toSample - fromSample); + + LinkedList<TimeToSampleBox.Entry> returnDecodingEntries = new LinkedList<TimeToSampleBox.Entry>(); + + for (long nuDecodingTime : nuDecodingTimes) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getDelta() != nuDecodingTime) { + TimeToSampleBox.Entry e = new TimeToSampleBox.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + TimeToSampleBox.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + if (origTrack.getCompositionTimeEntries() != null && !origTrack.getCompositionTimeEntries().isEmpty()) { + int[] compositionTime = CompositionTimeToSample.blowupCompositionTimes(origTrack.getCompositionTimeEntries()); + int[] nuCompositionTimes = new int[toSample - fromSample]; + System.arraycopy(compositionTime, fromSample, nuCompositionTimes, 0, toSample - fromSample); + + LinkedList<CompositionTimeToSample.Entry> returnDecodingEntries = new LinkedList<CompositionTimeToSample.Entry>(); + + for (int nuDecodingTime : nuCompositionTimes) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getOffset() != nuDecodingTime) { + CompositionTimeToSample.Entry e = new CompositionTimeToSample.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + CompositionTimeToSample.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + synchronized public long[] getSyncSamples() { + if (this.syncSampleArray == null) { + if (origTrack.getSyncSamples() != null && origTrack.getSyncSamples().length > 0) { + List<Long> syncSamples = new LinkedList<Long>(); + for (long l : origTrack.getSyncSamples()) { + if (l >= fromSample && l < toSample) { + syncSamples.add(l - fromSample); + } + } + syncSampleArray = new long[syncSamples.size()]; + for (int i = 0; i < syncSampleArray.length; i++) { + syncSampleArray[i] = syncSamples.get(i); + + } + return syncSampleArray; + } else { + return null; + } + } else { + return this.syncSampleArray; + } + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + if (origTrack.getSampleDependencies() != null && !origTrack.getSampleDependencies().isEmpty()) { + return origTrack.getSampleDependencies().subList(fromSample, toSample); + } else { + return null; + } + } + + public TrackMetaData getTrackMetaData() { + return origTrack.getTrackMetaData(); + } + + public String getHandler() { + return origTrack.getHandler(); + } + + public Box getMediaHeaderBox() { + return origTrack.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return origTrack.getSubsampleInformationBox(); + } + +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/DivideTimeScaleTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/DivideTimeScaleTrack.java.svn-base new file mode 100644 index 0000000..c51e8e0 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/DivideTimeScaleTrack.java.svn-base @@ -0,0 +1,126 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class DivideTimeScaleTrack implements Track { + Track source; + private int timeScaleDivisor; + + public DivideTimeScaleTrack(Track source, int timeScaleDivisor) { + this.source = source; + this.timeScaleDivisor = timeScaleDivisor; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return adjustTts(); + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return adjustCtts(); + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(source.getTrackMetaData().getTimescale() / this.timeScaleDivisor); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + List<CompositionTimeToSample.Entry> adjustCtts() { + List<CompositionTimeToSample.Entry> origCtts = this.source.getCompositionTimeEntries(); + if (origCtts != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(origCtts.size()); + for (CompositionTimeToSample.Entry entry : origCtts) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), entry.getOffset() / timeScaleDivisor)); + } + return entries2; + } else { + return null; + } + } + + List<TimeToSampleBox.Entry> adjustTts() { + List<TimeToSampleBox.Entry> origTts = source.getDecodingTimeEntries(); + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (TimeToSampleBox.Entry e : origTts) { + entries2.add(new TimeToSampleBox.Entry(e.getCount(), e.getDelta() / timeScaleDivisor)); + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "MultiplyTimeScaleTrack{" + + "source=" + source + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/EC3TrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/EC3TrackImpl.java.svn-base new file mode 100644 index 0000000..d0b2d76 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/EC3TrackImpl.java.svn-base @@ -0,0 +1,436 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.EC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.BitReaderBuffer; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by IntelliJ IDEA. + * User: magnus + * Date: 2012-03-14 + * Time: 10:39 + * To change this template use File | Settings | File Templates. + */ +public class EC3TrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int frameSize; + + List<BitStreamInfo> entries = new LinkedList<BitStreamInfo>(); + + private BufferedInputStream inputStream; + private List<ByteBuffer> samples; + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + private String lang = "und"; + + public EC3TrackImpl(InputStream fin, String lang) throws IOException { + this.lang = lang; + parse(fin); + } + + public EC3TrackImpl(InputStream fin) throws IOException { + parse(fin); + } + + private void parse(InputStream fin) throws IOException { + inputStream = new BufferedInputStream(fin); + + boolean done = false; + inputStream.mark(10000); + while (!done) { + BitStreamInfo bsi = readVariables(); + if (bsi == null) { + throw new IOException(); + } + for (BitStreamInfo entry : entries) { + if (bsi.strmtyp != 1 && entry.substreamid == bsi.substreamid) { + done = true; + } + } + if (!done) { + entries.add(bsi); + long skipped = inputStream.skip(bsi.frameSize); + assert skipped == bsi.frameSize; + } + } + + inputStream.reset(); + + if (entries.size() == 0) { + throw new IOException(); + } + samplerate = entries.get(0).samplerate; + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("ec-3"); + audioSampleEntry.setChannelCount(2); // According to ETSI TS 102 366 Annex F + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + EC3SpecificBox ec3 = new EC3SpecificBox(); + int[] deps = new int[entries.size()]; + int[] chan_locs = new int[entries.size()]; + for (BitStreamInfo bsi : entries) { + if (bsi.strmtyp == 1) { + deps[bsi.substreamid]++; + chan_locs[bsi.substreamid] = ((bsi.chanmap >> 6) & 0x100) | ((bsi.chanmap >> 5) & 0xff); + } + } + for (BitStreamInfo bsi : entries) { + if (bsi.strmtyp != 1) { + EC3SpecificBox.Entry e = new EC3SpecificBox.Entry(); + e.fscod = bsi.fscod; + e.bsid = bsi.bsid; + e.bsmod = bsi.bsmod; + e.acmod = bsi.acmod; + e.lfeon = bsi.lfeon; + e.reserved = 0; + e.num_dep_sub = deps[bsi.substreamid]; + e.chan_loc = chan_locs[bsi.substreamid]; + e.reserved2 = 0; + ec3.addEntry(e); + } + bitrate += bsi.bitrate; + frameSize += bsi.frameSize; + } + + ec3.setDataRate(bitrate / 1000); + audioSampleEntry.addBox(ec3); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + } + + + public List<ByteBuffer> getSamples() { + + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private BitStreamInfo readVariables() throws IOException { + byte[] data = new byte[200]; + inputStream.mark(200); + if (200 != inputStream.read(data, 0, 200)) { + return null; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(16); + if (syncword != 0xb77) { + return null; + } + + BitStreamInfo entry = new BitStreamInfo(); + + entry.strmtyp = brb.readBits(2); + entry.substreamid = brb.readBits(3); + int frmsiz = brb.readBits(11); + entry.frameSize = 2 * (frmsiz + 1); + + entry.fscod = brb.readBits(2); + int fscod2 = -1; + int numblkscod; + if (entry.fscod == 3) { + fscod2 = brb.readBits(2); + numblkscod = 3; + } else { + numblkscod = brb.readBits(2); + } + int numberOfBlocksPerSyncFrame = 0; + switch (numblkscod) { + case 0: + numberOfBlocksPerSyncFrame = 1; + break; + + case 1: + numberOfBlocksPerSyncFrame = 2; + break; + + case 2: + numberOfBlocksPerSyncFrame = 3; + break; + + case 3: + numberOfBlocksPerSyncFrame = 6; + break; + + } + entry.frameSize *= (6 / numberOfBlocksPerSyncFrame); + + entry.acmod = brb.readBits(3); + entry.lfeon = brb.readBits(1); + entry.bsid = brb.readBits(5); + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(8); // compr + } + if (0 == entry.acmod) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(8); + } + } + if (1 == entry.strmtyp) { + if (1 == brb.readBits(1)) { + entry.chanmap = brb.readBits(16); + } + } + if (1 == brb.readBits(1)) { // mixing metadata + if (entry.acmod > 2) { + brb.readBits(2); + } + if (1 == (entry.acmod & 1) && entry.acmod > 2) { + brb.readBits(3); + brb.readBits(3); + } + if (0 < (entry.acmod & 4)) { + brb.readBits(3); + brb.readBits(3); + } + if (1 == entry.lfeon) { + if (1 == brb.readBits(1)) { + brb.readBits(5); + } + } + if (0 == entry.strmtyp) { + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + if (0 == entry.acmod) { + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + } + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + int mixdef = brb.readBits(2); + if (1 == mixdef) { + brb.readBits(5); + } else if (2 == mixdef) { + brb.readBits(12); + } else if (3 == mixdef) { + int mixdeflen = brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + } + } + if (1 == brb.readBits(1)) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(7); + if (1 == brb.readBits(1)) { + brb.readBits(8); + } + } + } + for (int i = 0; i < (mixdeflen + 2); i++) { + brb.readBits(8); + } + brb.byteSync(); + } + if (entry.acmod < 2) { + if (1 == brb.readBits(1)) { + brb.readBits(14); + } + if (0 == entry.acmod) { + if (1 == brb.readBits(1)) { + brb.readBits(14); + } + } + if (1 == brb.readBits(1)) { + if (numblkscod == 0) { + brb.readBits(5); + } else { + for (int i = 0; i < numberOfBlocksPerSyncFrame; i++) { + if (1 == brb.readBits(1)) { + brb.readBits(5); + } + } + } + + } + } + } + } + if (1 == brb.readBits(1)) { // infomdate + entry.bsmod = brb.readBits(3); + } + + switch (entry.fscod) { + case 0: + entry.samplerate = 48000; + break; + + case 1: + entry.samplerate = 44100; + break; + + case 2: + entry.samplerate = 32000; + break; + + case 3: { + switch (fscod2) { + case 0: + entry.samplerate = 24000; + break; + + case 1: + entry.samplerate = 22050; + break; + + case 2: + entry.samplerate = 16000; + break; + + case 3: + entry.samplerate = 0; + break; + } + break; + } + + } + if (entry.samplerate == 0) { + return null; + } + + entry.bitrate = (int) (((double) entry.samplerate) / 1536.0 * entry.frameSize * 8); + + return entry; + } + + private boolean readSamples() throws IOException { + int read = frameSize; + boolean ret = false; + while (frameSize == read) { + ret = true; + byte[] data = new byte[frameSize]; + read = inputStream.read(data); + if (read == frameSize) { + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1536)); + } + } + return ret; + } + + public static class BitStreamInfo extends EC3SpecificBox.Entry { + public int frameSize; + public int substreamid; + public int bitrate; + public int samplerate; + public int strmtyp; + public int chanmap; + + @Override + public String toString() { + return "BitStreamInfo{" + + "frameSize=" + frameSize + + ", substreamid=" + substreamid + + ", bitrate=" + bitrate + + ", samplerate=" + samplerate + + ", strmtyp=" + strmtyp + + ", chanmap=" + chanmap + + '}'; + } + } + + @Override + public String toString() { + return "EC3TrackImpl{" + + "bitrate=" + bitrate + + ", samplerate=" + samplerate + + ", entries=" + entries + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/H264TrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/H264TrackImpl.java.svn-base new file mode 100644 index 0000000..b3c1866 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/H264TrackImpl.java.svn-base @@ -0,0 +1,740 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.h264.model.PictureParameterSet; +import com.googlecode.mp4parser.h264.model.SeqParameterSet; +import com.googlecode.mp4parser.h264.read.CAVLCReader; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +/** + * The <code>H264TrackImpl</code> creates a <code>Track</code> from an H.264 + * Annex B file. + */ +public class H264TrackImpl extends AbstractTrack { + private static final Logger LOG = Logger.getLogger(H264TrackImpl.class.getName()); + + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + private ReaderWrapper reader; + private List<ByteBuffer> samples; + boolean readSamples = false; + + List<TimeToSampleBox.Entry> stts; + List<CompositionTimeToSample.Entry> ctts; + List<SampleDependencyTypeBox.Entry> sdtp; + List<Integer> stss; + + SeqParameterSet seqParameterSet = null; + PictureParameterSet pictureParameterSet = null; + LinkedList<byte[]> seqParameterSetList = new LinkedList<byte[]>(); + LinkedList<byte[]> pictureParameterSetList = new LinkedList<byte[]>(); + + private int width; + private int height; + private int timescale; + private int frametick; + private int currentScSize; + private int prevScSize; + + private SEIMessage seiMessage; + int frameNrInGop = 0; + private boolean determineFrameRate = true; + private String lang = "und"; + + public H264TrackImpl(InputStream inputStream, String lang, long timescale) throws IOException { + this.lang = lang; + if (timescale > 1000) { + timescale = timescale; //e.g. 23976 + frametick = 1000; + determineFrameRate = false; + } else { + throw new IllegalArgumentException("Timescale must be specified in milliseconds!"); + } + parse(inputStream); + } + + public H264TrackImpl(InputStream inputStream, String lang) throws IOException { + this.lang = lang; + parse(inputStream); + } + + public H264TrackImpl(InputStream inputStream) throws IOException { + parse(inputStream); + } + + private void parse(InputStream inputStream) throws IOException { + this.reader = new ReaderWrapper(inputStream); + stts = new LinkedList<TimeToSampleBox.Entry>(); + ctts = new LinkedList<CompositionTimeToSample.Entry>(); + sdtp = new LinkedList<SampleDependencyTypeBox.Entry>(); + stss = new LinkedList<Integer>(); + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + + if (!readVariables()) { + throw new IOException(); + } + + sampleDescriptionBox = new SampleDescriptionBox(); + VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1"); + visualSampleEntry.setDataReferenceIndex(1); + visualSampleEntry.setDepth(24); + visualSampleEntry.setFrameCount(1); + visualSampleEntry.setHorizresolution(72); + visualSampleEntry.setVertresolution(72); + visualSampleEntry.setWidth(width); + visualSampleEntry.setHeight(height); + visualSampleEntry.setCompressorname("AVC Coding"); + + AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox(); + + avcConfigurationBox.setSequenceParameterSets(seqParameterSetList); + avcConfigurationBox.setPictureParameterSets(pictureParameterSetList); + avcConfigurationBox.setAvcLevelIndication(seqParameterSet.level_idc); + avcConfigurationBox.setAvcProfileIndication(seqParameterSet.profile_idc); + avcConfigurationBox.setBitDepthLumaMinus8(seqParameterSet.bit_depth_luma_minus8); + avcConfigurationBox.setBitDepthChromaMinus8(seqParameterSet.bit_depth_chroma_minus8); + avcConfigurationBox.setChromaFormat(seqParameterSet.chroma_format_idc.getId()); + avcConfigurationBox.setConfigurationVersion(1); + avcConfigurationBox.setLengthSizeMinusOne(3); + avcConfigurationBox.setProfileCompatibility(seqParameterSetList.get(0)[1]); + + visualSampleEntry.addBox(avcConfigurationBox); + sampleDescriptionBox.addBox(visualSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(timescale); + trackMetaData.setWidth(width); + trackMetaData.setHeight(height); + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return ctts; + } + + public long[] getSyncSamples() { + long[] returns = new long[stss.size()]; + for (int i = 0; i < stss.size(); i++) { + returns[i] = stss.get(i); + } + return returns; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return sdtp; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "vide"; + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new VideoMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() { + width = (seqParameterSet.pic_width_in_mbs_minus1 + 1) * 16; + int mult = 2; + if (seqParameterSet.frame_mbs_only_flag) { + mult = 1; + } + height = 16 * (seqParameterSet.pic_height_in_map_units_minus1 + 1) * mult; + if (seqParameterSet.frame_cropping_flag) { + int chromaArrayType = 0; + if (seqParameterSet.residual_color_transform_flag == false) { + chromaArrayType = seqParameterSet.chroma_format_idc.getId(); + } + int cropUnitX = 1; + int cropUnitY = mult; + if (chromaArrayType != 0) { + cropUnitX = seqParameterSet.chroma_format_idc.getSubWidth(); + cropUnitY = seqParameterSet.chroma_format_idc.getSubHeight() * mult; + } + + width -= cropUnitX * (seqParameterSet.frame_crop_left_offset + seqParameterSet.frame_crop_right_offset); + height -= cropUnitY * (seqParameterSet.frame_crop_top_offset + seqParameterSet.frame_crop_bottom_offset); + } + return true; + } + + private boolean findNextStartcode() throws IOException { + byte[] test = new byte[]{-1, -1, -1, -1}; + + int c; + while ((c = reader.read()) != -1) { + test[0] = test[1]; + test[1] = test[2]; + test[2] = test[3]; + test[3] = (byte) c; + if (test[0] == 0 && test[1] == 0 && test[2] == 0 && test[3] == 1) { + prevScSize = currentScSize; + currentScSize = 4; + return true; + } + if (test[0] == 0 && test[1] == 0 && test[2] == 1) { + prevScSize = currentScSize; + currentScSize = 3; + return true; + } + } + return false; + } + + private enum NALActions { + IGNORE, BUFFER, STORE, END + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + + readSamples = true; + + + findNextStartcode(); + reader.mark(); + long pos = reader.getPos(); + + ArrayList<byte[]> buffered = new ArrayList<byte[]>(); + + int frameNr = 0; + + while (findNextStartcode()) { + long newpos = reader.getPos(); + int size = (int) (newpos - pos - prevScSize); + reader.reset(); + byte[] data = new byte[size ]; + reader.read(data); + int type = data[0]; + int nal_ref_idc = (type >> 5) & 3; + int nal_unit_type = type & 0x1f; + LOG.fine("Found startcode at " + (pos -4) + " Type: " + nal_unit_type + " ref idc: " + nal_ref_idc + " (size " + size + ")"); + NALActions action = handleNALUnit(nal_ref_idc, nal_unit_type, data); + switch (action) { + case IGNORE: + break; + + case BUFFER: + buffered.add(data); + break; + + case STORE: + int stdpValue = 22; + frameNr++; + buffered.add(data); + ByteBuffer bb = createSample(buffered); + boolean IdrPicFlag = false; + if (nal_unit_type == 5) { + stdpValue += 16; + IdrPicFlag = true; + } + ByteArrayInputStream bs = cleanBuffer(buffered.get(buffered.size() - 1)); + SliceHeader sh = new SliceHeader(bs, seqParameterSet, pictureParameterSet, IdrPicFlag); + if (sh.slice_type == SliceHeader.SliceType.B) { + stdpValue += 4; + } + LOG.fine("Adding sample with size " + bb.capacity() + " and header " + sh); + buffered.clear(); + samples.add(bb); + stts.add(new TimeToSampleBox.Entry(1, frametick)); + if (nal_unit_type == 5) { // IDR Picture + stss.add(frameNr); + } + if (seiMessage.n_frames == 0) { + frameNrInGop = 0; + } + int offset = 0; + if (seiMessage.clock_timestamp_flag) { + offset = seiMessage.n_frames - frameNrInGop; + } else if (seiMessage.removal_delay_flag) { + offset = seiMessage.dpb_removal_delay / 2; + } + ctts.add(new CompositionTimeToSample.Entry(1, offset * frametick)); + sdtp.add(new SampleDependencyTypeBox.Entry(stdpValue)); + frameNrInGop++; + break; + + case END: + return true; + + + } + pos = newpos; + reader.seek(currentScSize); + reader.mark(); + } + return true; + } + + private ByteBuffer createSample(List<byte[]> buffers) { + int outsize = 0; + for (int i = 0; i < buffers.size(); i++) { + outsize += buffers.get(i).length + 4; + } + byte[] output = new byte[outsize]; + + ByteBuffer bb = ByteBuffer.wrap(output); + for (int i = 0; i < buffers.size(); i++) { + bb.putInt(buffers.get(i).length); + bb.put(buffers.get(i)); + } + bb.rewind(); + return bb; + } + + private ByteArrayInputStream cleanBuffer(byte[] data) { + byte[] output = new byte[data.length]; + int inPos = 0; + int outPos = 0; + while (inPos < data.length) { + if (data[inPos] == 0 && data[inPos + 1] == 0 && data[inPos + 2] == 3) { + output[outPos] = 0; + output[outPos + 1] = 0; + inPos += 3; + outPos += 2; + } else { + output[outPos] = data[inPos]; + inPos++; + outPos++; + } + } + return new ByteArrayInputStream(output, 0, outPos); + } + + private NALActions handleNALUnit(int nal_ref_idc, int nal_unit_type, byte[] data) throws IOException { + NALActions action; + switch (nal_unit_type) { + case 1: + case 2: + case 3: + case 4: + case 5: + action = NALActions.STORE; // Will only work in single slice per frame mode! + break; + + case 6: + seiMessage = new SEIMessage(cleanBuffer(data), seqParameterSet); + action = NALActions.BUFFER; + break; + + case 9: +// printAccessUnitDelimiter(data); + int type = data[1] >> 5; + LOG.fine("Access unit delimiter type: " + type); + action = NALActions.BUFFER; + break; + + + case 7: + if (seqParameterSet == null) { + ByteArrayInputStream is = cleanBuffer(data); + is.read(); + seqParameterSet = SeqParameterSet.read(is); + seqParameterSetList.add(data); + configureFramerate(); + } + action = NALActions.IGNORE; + break; + + case 8: + if (pictureParameterSet == null) { + ByteArrayInputStream is = new ByteArrayInputStream(data); + is.read(); + pictureParameterSet = PictureParameterSet.read(is); + pictureParameterSetList.add(data); + } + action = NALActions.IGNORE; + break; + + case 10: + case 11: + action = NALActions.END; + break; + + default: + System.err.println("Unknown NAL unit type: " + nal_unit_type); + action = NALActions.IGNORE; + + } + + return action; + } + + private void configureFramerate() { + if (determineFrameRate) { + if (seqParameterSet.vuiParams != null) { + timescale = seqParameterSet.vuiParams.time_scale >> 1; // Not sure why, but I found this in several places, and it works... + frametick = seqParameterSet.vuiParams.num_units_in_tick; + if (timescale == 0 || frametick == 0) { + System.err.println("Warning: vuiParams contain invalid values: time_scale: " + timescale + " and frame_tick: " + frametick + ". Setting frame rate to 25fps"); + timescale = 90000; + frametick = 3600; + } + } else { + System.err.println("Warning: Can't determine frame rate. Guessing 25 fps"); + timescale = 90000; + frametick = 3600; + } + } + } + + public void printAccessUnitDelimiter(byte[] data) { + LOG.fine("Access unit delimiter: " + (data[1] >> 5)); + } + + public static class SliceHeader { + + public enum SliceType { + P, B, I, SP, SI + } + + public int first_mb_in_slice; + public SliceType slice_type; + public int pic_parameter_set_id; + public int colour_plane_id; + public int frame_num; + public boolean field_pic_flag = false; + public boolean bottom_field_flag = false; + public int idr_pic_id; + public int pic_order_cnt_lsb; + public int delta_pic_order_cnt_bottom; + + public SliceHeader(InputStream is, SeqParameterSet sps, PictureParameterSet pps, boolean IdrPicFlag) throws IOException { + is.read(); + CAVLCReader reader = new CAVLCReader(is); + first_mb_in_slice = reader.readUE("SliceHeader: first_mb_in_slice"); + switch (reader.readUE("SliceHeader: slice_type")) { + case 0: + case 5: + slice_type = SliceType.P; + break; + + case 1: + case 6: + slice_type = SliceType.B; + break; + + case 2: + case 7: + slice_type = SliceType.I; + break; + + case 3: + case 8: + slice_type = SliceType.SP; + break; + + case 4: + case 9: + slice_type = SliceType.SI; + break; + + } + pic_parameter_set_id = reader.readUE("SliceHeader: pic_parameter_set_id"); + if (sps.residual_color_transform_flag) { + colour_plane_id = reader.readU(2, "SliceHeader: colour_plane_id"); + } + frame_num = reader.readU(sps.log2_max_frame_num_minus4 + 4, "SliceHeader: frame_num"); + + if (!sps.frame_mbs_only_flag) { + field_pic_flag = reader.readBool("SliceHeader: field_pic_flag"); + if (field_pic_flag) { + bottom_field_flag = reader.readBool("SliceHeader: bottom_field_flag"); + } + } + if (IdrPicFlag) { + idr_pic_id = reader.readUE("SliceHeader: idr_pic_id"); + if (sps.pic_order_cnt_type == 0) { + pic_order_cnt_lsb = reader.readU(sps.log2_max_pic_order_cnt_lsb_minus4 + 4, "SliceHeader: pic_order_cnt_lsb"); + if (pps.pic_order_present_flag && !field_pic_flag) { + delta_pic_order_cnt_bottom = reader.readSE("SliceHeader: delta_pic_order_cnt_bottom"); + } + } + } + } + + @Override + public String toString() { + return "SliceHeader{" + + "first_mb_in_slice=" + first_mb_in_slice + + ", slice_type=" + slice_type + + ", pic_parameter_set_id=" + pic_parameter_set_id + + ", colour_plane_id=" + colour_plane_id + + ", frame_num=" + frame_num + + ", field_pic_flag=" + field_pic_flag + + ", bottom_field_flag=" + bottom_field_flag + + ", idr_pic_id=" + idr_pic_id + + ", pic_order_cnt_lsb=" + pic_order_cnt_lsb + + ", delta_pic_order_cnt_bottom=" + delta_pic_order_cnt_bottom + + '}'; + } + } + + private class ReaderWrapper { + private InputStream inputStream; + private long pos = 0; + + private long markPos = 0; + + + private ReaderWrapper(InputStream inputStream) { + this.inputStream = inputStream; + } + + int read() throws IOException { + pos++; + return inputStream.read(); + } + + long read(byte[] data) throws IOException { + long read = inputStream.read(data); + pos += read; + return read; + } + + long seek(int dist) throws IOException { + long seeked = inputStream.skip(dist); + pos += seeked; + return seeked; + } + + public long getPos() { + return pos; + } + + public void mark() { + int i = 1048576; + LOG.fine("Marking with " + i + " at " + pos); + inputStream.mark(i); + markPos = pos; + } + + + public void reset() throws IOException { + long diff = pos - markPos; + LOG.fine("Resetting to " + markPos + " (pos is " + pos + ") which makes the buffersize " + diff); + inputStream.reset(); + pos = markPos; + } + } + + public class SEIMessage { + + int payloadType = 0; + int payloadSize = 0; + + boolean removal_delay_flag; + int cpb_removal_delay; + int dpb_removal_delay; + + boolean clock_timestamp_flag; + int pic_struct; + int ct_type; + int nuit_field_based_flag; + int counting_type; + int full_timestamp_flag; + int discontinuity_flag; + int cnt_dropped_flag; + int n_frames; + int seconds_value; + int minutes_value; + int hours_value; + int time_offset_length; + int time_offset; + + SeqParameterSet sps; + + public SEIMessage(InputStream is, SeqParameterSet sps) throws IOException { + this.sps = sps; + is.read(); + int datasize = is.available(); + int read = 0; + while (read < datasize) { + payloadType = 0; + payloadSize = 0; + int last_payload_type_bytes = is.read(); + read++; + while (last_payload_type_bytes == 0xff) { + payloadType += last_payload_type_bytes; + last_payload_type_bytes = is.read(); + read++; + } + payloadType += last_payload_type_bytes; + int last_payload_size_bytes = is.read(); + read++; + + while (last_payload_size_bytes == 0xff) { + payloadSize += last_payload_size_bytes; + last_payload_size_bytes = is.read(); + read++; + } + payloadSize += last_payload_size_bytes; + if (datasize - read >= payloadSize) { + if (payloadType == 1) { // pic_timing is what we are interested in! + if (sps.vuiParams != null && (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null || sps.vuiParams.pic_struct_present_flag)) { + byte[] data = new byte[payloadSize]; + is.read(data); + read += payloadSize; + CAVLCReader reader = new CAVLCReader(new ByteArrayInputStream(data)); + if (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null) { + removal_delay_flag = true; + cpb_removal_delay = reader.readU(sps.vuiParams.nalHRDParams.cpb_removal_delay_length_minus1 + 1, "SEI: cpb_removal_delay"); + dpb_removal_delay = reader.readU(sps.vuiParams.nalHRDParams.dpb_output_delay_length_minus1 + 1, "SEI: dpb_removal_delay"); + } else { + removal_delay_flag = false; + } + if (sps.vuiParams.pic_struct_present_flag) { + pic_struct = reader.readU(4, "SEI: pic_struct"); + int numClockTS; + switch (pic_struct) { + case 0: + case 1: + case 2: + default: + numClockTS = 1; + break; + + case 3: + case 4: + case 7: + numClockTS = 2; + break; + + case 5: + case 6: + case 8: + numClockTS = 3; + break; + } + for (int i = 0; i < numClockTS; i++) { + clock_timestamp_flag = reader.readBool("pic_timing SEI: clock_timestamp_flag[" + i + "]"); + if (clock_timestamp_flag) { + ct_type = reader.readU(2, "pic_timing SEI: ct_type"); + nuit_field_based_flag = reader.readU(1, "pic_timing SEI: nuit_field_based_flag"); + counting_type = reader.readU(5, "pic_timing SEI: counting_type"); + full_timestamp_flag = reader.readU(1, "pic_timing SEI: full_timestamp_flag"); + discontinuity_flag = reader.readU(1, "pic_timing SEI: discontinuity_flag"); + cnt_dropped_flag = reader.readU(1, "pic_timing SEI: cnt_dropped_flag"); + n_frames = reader.readU(8, "pic_timing SEI: n_frames"); + if (full_timestamp_flag == 1) { + seconds_value = reader.readU(6, "pic_timing SEI: seconds_value"); + minutes_value = reader.readU(6, "pic_timing SEI: minutes_value"); + hours_value = reader.readU(5, "pic_timing SEI: hours_value"); + } else { + if (reader.readBool("pic_timing SEI: seconds_flag")) { + seconds_value = reader.readU(6, "pic_timing SEI: seconds_value"); + if (reader.readBool("pic_timing SEI: minutes_flag")) { + minutes_value = reader.readU(6, "pic_timing SEI: minutes_value"); + if (reader.readBool("pic_timing SEI: hours_flag")) { + hours_value = reader.readU(5, "pic_timing SEI: hours_value"); + } + } + } + } + if (true) { + if (sps.vuiParams.nalHRDParams != null) { + time_offset_length = sps.vuiParams.nalHRDParams.time_offset_length; + } else if (sps.vuiParams.vclHRDParams != null) { + time_offset_length = sps.vuiParams.vclHRDParams.time_offset_length; + } else { + time_offset_length = 24; + } + time_offset = reader.readU(24, "pic_timing SEI: time_offset"); + } + } + } + } + + } else { + for (int i = 0; i < payloadSize; i++) { + is.read(); + read++; + } + } + } else { + for (int i = 0; i < payloadSize; i++) { + is.read(); + read++; + } + } + } else { + read = datasize; + } + LOG.fine(this.toString()); + } + } + + @Override + public String toString() { + String out = "SEIMessage{" + + "payloadType=" + payloadType + + ", payloadSize=" + payloadSize; + if (payloadType == 1) { + if (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null) { + + out += ", cpb_removal_delay=" + cpb_removal_delay + + ", dpb_removal_delay=" + dpb_removal_delay; + } + if (sps.vuiParams.pic_struct_present_flag) { + out += ", pic_struct=" + pic_struct; + if (clock_timestamp_flag) { + out += ", ct_type=" + ct_type + + ", nuit_field_based_flag=" + nuit_field_based_flag + + ", counting_type=" + counting_type + + ", full_timestamp_flag=" + full_timestamp_flag + + ", discontinuity_flag=" + discontinuity_flag + + ", cnt_dropped_flag=" + cnt_dropped_flag + + ", n_frames=" + n_frames + + ", seconds_value=" + seconds_value + + ", minutes_value=" + minutes_value + + ", hours_value=" + hours_value + + ", time_offset_length=" + time_offset_length + + ", time_offset=" + time_offset; + } + } + } + out += '}'; + return out; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/MultiplyTimeScaleTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/MultiplyTimeScaleTrack.java.svn-base new file mode 100644 index 0000000..e9a90e4 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/MultiplyTimeScaleTrack.java.svn-base @@ -0,0 +1,130 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; +import static com.googlecode.mp4parser.util.Math.gcd; +import static com.googlecode.mp4parser.util.Math.lcm; +import static java.lang.Math.round; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class MultiplyTimeScaleTrack implements Track { + Track source; + private int timeScaleFactor; + + public MultiplyTimeScaleTrack(Track source, int timeScaleFactor) { + this.source = source; + this.timeScaleFactor = timeScaleFactor; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return adjustTts(source.getDecodingTimeEntries(), timeScaleFactor); + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return adjustCtts(source.getCompositionTimeEntries(), timeScaleFactor); + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(source.getTrackMetaData().getTimescale() * this.timeScaleFactor); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + static List<CompositionTimeToSample.Entry> adjustCtts(List<CompositionTimeToSample.Entry> source, int timeScaleFactor) { + if (source != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(source.size()); + for (CompositionTimeToSample.Entry entry : source) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), timeScaleFactor * entry.getOffset())); + } + return entries2; + } else { + return null; + } + } + + static List<TimeToSampleBox.Entry> adjustTts(List<TimeToSampleBox.Entry> source, int timeScaleFactor) { + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (TimeToSampleBox.Entry e : source) { + entries2.add(new TimeToSampleBox.Entry(e.getCount(), timeScaleFactor * e.getDelta())); + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "MultiplyTimeScaleTrack{" + + "source=" + source + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/QuicktimeTextTrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/QuicktimeTextTrackImpl.java.svn-base new file mode 100644 index 0000000..8efa399 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/QuicktimeTextTrackImpl.java.svn-base @@ -0,0 +1,165 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.TextSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.apple.BaseMediaInfoAtom; +import com.googlecode.mp4parser.boxes.apple.GenericMediaHeaderAtom; +import com.googlecode.mp4parser.boxes.apple.GenericMediaHeaderTextAtom; +import com.googlecode.mp4parser.boxes.apple.QuicktimeTextSampleEntry; +import com.googlecode.mp4parser.boxes.threegpp26245.FontTableBox; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * A Text track as Quicktime Pro would create. + */ +public class QuicktimeTextTrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + List<Line> subs = new LinkedList<Line>(); + + public List<Line> getSubs() { + return subs; + } + + public QuicktimeTextTrackImpl() { + sampleDescriptionBox = new SampleDescriptionBox(); + QuicktimeTextSampleEntry textTrack = new QuicktimeTextSampleEntry(); + textTrack.setDataReferenceIndex(1); + sampleDescriptionBox.addBox(textTrack); + + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); + + + } + + + public List<ByteBuffer> getSamples() { + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + samples.add(ByteBuffer.wrap(new byte[]{0, 0})); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + dos.writeShort(sub.text.getBytes("UTF-8").length); + dos.write(sub.text.getBytes("UTF-8")); + dos.close(); + } catch (IOException e) { + throw new Error("VM is broken. Does not support UTF-8"); + } + samples.add(ByteBuffer.wrap(baos.toByteArray())); + lastEnd = sub.to; + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + stts.add(new TimeToSampleBox.Entry(1, silentTime)); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + stts.add(new TimeToSampleBox.Entry(1, sub.to - sub.from)); + lastEnd = sub.to; + } + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "text"; + } + + + public static class Line { + long from; + long to; + String text; + + + public Line(long from, long to, String text) { + this.from = from; + this.to = to; + this.text = text; + } + + public long getFrom() { + return from; + } + + public String getText() { + return text; + } + + public long getTo() { + return to; + } + } + + public Box getMediaHeaderBox() { + GenericMediaHeaderAtom ghmd = new GenericMediaHeaderAtom(); + ghmd.addBox(new BaseMediaInfoAtom()); + ghmd.addBox(new GenericMediaHeaderTextAtom()); + return ghmd; + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ReplaceSampleTrack.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ReplaceSampleTrack.java.svn-base new file mode 100644 index 0000000..81a129d --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/ReplaceSampleTrack.java.svn-base @@ -0,0 +1,104 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.AbstractList; +import java.util.LinkedList; +import java.util.List; + +/** + * Generates a Track where a single sample has been replaced by a given <code>ByteBuffer</code>. + */ + +public class ReplaceSampleTrack extends AbstractTrack { + Track origTrack; + private long sampleNumber; + private ByteBuffer sampleContent; + private List<ByteBuffer> samples; + + public ReplaceSampleTrack(Track origTrack, long sampleNumber, ByteBuffer content) { + this.origTrack = origTrack; + this.sampleNumber = sampleNumber; + this.sampleContent = content; + this.samples = new ReplaceASingleEntryList(); + + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return origTrack.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return origTrack.getDecodingTimeEntries(); + + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return origTrack.getCompositionTimeEntries(); + + } + + synchronized public long[] getSyncSamples() { + return origTrack.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return origTrack.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + return origTrack.getTrackMetaData(); + } + + public String getHandler() { + return origTrack.getHandler(); + } + + public Box getMediaHeaderBox() { + return origTrack.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return origTrack.getSubsampleInformationBox(); + } + + private class ReplaceASingleEntryList extends AbstractList<ByteBuffer> { + @Override + public ByteBuffer get(int index) { + if (ReplaceSampleTrack.this.sampleNumber == index) { + return ReplaceSampleTrack.this.sampleContent; + } else { + return ReplaceSampleTrack.this.origTrack.getSamples().get(index); + } + } + + @Override + public int size() { + return ReplaceSampleTrack.this.origTrack.getSamples().size(); + } + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/SilenceTrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/SilenceTrackImpl.java.svn-base new file mode 100644 index 0000000..f74ab3c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/SilenceTrackImpl.java.svn-base @@ -0,0 +1,98 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Mp4TrackImpl; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * This is just a basic idea how things could work but they don't. + */ +public class SilenceTrackImpl implements Track { + Track source; + + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + TimeToSampleBox.Entry entry; + + public SilenceTrackImpl(Track ofType, long ms) { + source = ofType; + if ("mp4a".equals(ofType.getSampleDescriptionBox().getSampleEntry().getType())) { + long numFrames = getTrackMetaData().getTimescale() * ms / 1000 / 1024; + long standZeit = getTrackMetaData().getTimescale() * ms / numFrames / 1000; + entry = new TimeToSampleBox.Entry(numFrames, standZeit); + + + while (numFrames-- > 0) { + samples.add((ByteBuffer) ByteBuffer.wrap(new byte[]{ + 0x21, 0x10, 0x04, 0x60, (byte) 0x8c, 0x1c, + }).rewind()); + } + + } else { + throw new RuntimeException("Tracks of type " + ofType.getClass().getSimpleName() + " are not supported"); + } + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return Collections.singletonList(entry); + + } + + public TrackMetaData getTrackMetaData() { + return source.getTrackMetaData(); + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/TextTrackImpl.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/TextTrackImpl.java.svn-base new file mode 100644 index 0000000..3bae143 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/.svn/text-base/TextTrackImpl.java.svn-base @@ -0,0 +1,165 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.TextSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.threegpp26245.FontTableBox; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +public class TextTrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + List<Line> subs = new LinkedList<Line>(); + + public List<Line> getSubs() { + return subs; + } + + public TextTrackImpl() { + sampleDescriptionBox = new SampleDescriptionBox(); + TextSampleEntry tx3g = new TextSampleEntry("tx3g"); + tx3g.setDataReferenceIndex(1); + tx3g.setStyleRecord(new TextSampleEntry.StyleRecord()); + tx3g.setBoxRecord(new TextSampleEntry.BoxRecord()); + sampleDescriptionBox.addBox(tx3g); + + FontTableBox ftab = new FontTableBox(); + ftab.setEntries(Collections.singletonList(new FontTableBox.FontRecord(1, "Serif"))); + + tx3g.addBox(ftab); + + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); // Text tracks use millieseconds + + + } + + + public List<ByteBuffer> getSamples() { + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + samples.add(ByteBuffer.wrap(new byte[]{0, 0})); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + dos.writeShort(sub.text.getBytes("UTF-8").length); + dos.write(sub.text.getBytes("UTF-8")); + dos.close(); + } catch (IOException e) { + throw new Error("VM is broken. Does not support UTF-8"); + } + samples.add(ByteBuffer.wrap(baos.toByteArray())); + lastEnd = sub.to; + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + stts.add(new TimeToSampleBox.Entry(1, silentTime)); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + stts.add(new TimeToSampleBox.Entry(1, sub.to - sub.from)); + lastEnd = sub.to; + } + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "sbtl"; + } + + + public static class Line { + long from; + long to; + String text; + + + public Line(long from, long to, String text) { + this.from = from; + this.to = to; + this.text = text; + } + + public long getFrom() { + return from; + } + + public String getText() { + return text; + } + + public long getTo() { + return to; + } + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new NullMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AACTrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AACTrackImpl.java new file mode 100644 index 0000000..df51a1a --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AACTrackImpl.java @@ -0,0 +1,292 @@ +/* + * Copyright 2012 castLabs GmbH, Berlin + * + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.AC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.*; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.*; + +/** + */ +public class AACTrackImpl extends AbstractTrack { + public static Map<Integer, Integer> samplingFrequencyIndexMap = new HashMap<Integer, Integer>(); + + static { + samplingFrequencyIndexMap.put(96000, 0); + samplingFrequencyIndexMap.put(88200, 1); + samplingFrequencyIndexMap.put(64000, 2); + samplingFrequencyIndexMap.put(48000, 3); + samplingFrequencyIndexMap.put(44100, 4); + samplingFrequencyIndexMap.put(32000, 5); + samplingFrequencyIndexMap.put(24000, 6); + samplingFrequencyIndexMap.put(22050, 7); + samplingFrequencyIndexMap.put(16000, 8); + samplingFrequencyIndexMap.put(12000, 9); + samplingFrequencyIndexMap.put(11025, 10); + samplingFrequencyIndexMap.put(8000, 11); + samplingFrequencyIndexMap.put(0x0, 96000); + samplingFrequencyIndexMap.put(0x1, 88200); + samplingFrequencyIndexMap.put(0x2, 64000); + samplingFrequencyIndexMap.put(0x3, 48000); + samplingFrequencyIndexMap.put(0x4, 44100); + samplingFrequencyIndexMap.put(0x5, 32000); + samplingFrequencyIndexMap.put(0x6, 24000); + samplingFrequencyIndexMap.put(0x7, 22050); + samplingFrequencyIndexMap.put(0x8, 16000); + samplingFrequencyIndexMap.put(0x9, 12000); + samplingFrequencyIndexMap.put(0xa, 11025); + samplingFrequencyIndexMap.put(0xb, 8000); + } + + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int channelCount; + int channelconfig; + + int bufferSizeDB; + long maxBitRate; + long avgBitRate; + + private BufferedInputStream inputStream; + private List<ByteBuffer> samples; + boolean readSamples = false; + List<TimeToSampleBox.Entry> stts; + private String lang = "und"; + + + public AACTrackImpl(InputStream inputStream, String lang) throws IOException { + this.lang = lang; + parse(inputStream); + } + + public AACTrackImpl(InputStream inputStream) throws IOException { + parse(inputStream); + } + + private void parse(InputStream inputStream) throws IOException { + this.inputStream = new BufferedInputStream(inputStream); + stts = new LinkedList<TimeToSampleBox.Entry>(); + + if (!readVariables()) { + throw new IOException(); + } + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + + double packetsPerSecond = (double)samplerate / 1024.0; + double duration = samples.size() / packetsPerSecond; + + long dataSize = 0; + LinkedList<Integer> queue = new LinkedList<Integer>(); + for (int i = 0; i < samples.size(); i++) { + int size = samples.get(i).capacity(); + dataSize += size; + queue.add(size); + while (queue.size() > packetsPerSecond) { + queue.pop(); + } + if (queue.size() == (int) packetsPerSecond) { + int currSize = 0; + for (int j = 0 ; j < queue.size(); j++) { + currSize += queue.get(j); + } + double currBitrate = 8.0 * currSize / queue.size() * packetsPerSecond; + if (currBitrate > maxBitRate) { + maxBitRate = (int)currBitrate; + } + } + } + + avgBitRate = (int) (8 * dataSize / duration); + + bufferSizeDB = 1536; /* TODO: Calcultate this somehow! */ + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a"); + audioSampleEntry.setChannelCount(2); + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + + ESDescriptorBox esds = new ESDescriptorBox(); + ESDescriptor descriptor = new ESDescriptor(); + descriptor.setEsId(0); + + SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor(); + slConfigDescriptor.setPredefined(2); + descriptor.setSlConfigDescriptor(slConfigDescriptor); + + DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor(); + decoderConfigDescriptor.setObjectTypeIndication(0x40); + decoderConfigDescriptor.setStreamType(5); + decoderConfigDescriptor.setBufferSizeDB(bufferSizeDB); + decoderConfigDescriptor.setMaxBitRate(maxBitRate); + decoderConfigDescriptor.setAvgBitRate(avgBitRate); + + AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig(); + audioSpecificConfig.setAudioObjectType(2); // AAC LC + audioSpecificConfig.setSamplingFrequencyIndex(samplingFrequencyIndexMap.get(samplerate)); + audioSpecificConfig.setChannelConfiguration(channelconfig); + decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig); + + descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor); + + ByteBuffer data = descriptor.serialize(); + esds.setData(data); + audioSampleEntry.addBox(esds); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public Box getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() throws IOException { + byte[] data = new byte[100]; + inputStream.mark(100); + if (100 != inputStream.read(data, 0, 100)) { + return false; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(12); + if (syncword != 0xfff) { + return false; + } + int id = brb.readBits(1); + int layer = brb.readBits(2); + int protectionAbsent = brb.readBits(1); + int profile = brb.readBits(2); + samplerate = samplingFrequencyIndexMap.get(brb.readBits(4)); + brb.readBits(1); + channelconfig = brb.readBits(3); + int original = brb.readBits(1); + int home = brb.readBits(1); + int emphasis = brb.readBits(2); + + return true; + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + + readSamples = true; + byte[] header = new byte[15]; + boolean ret = false; + inputStream.mark(15); + while (-1 != inputStream.read(header)) { + ret = true; + ByteBuffer bb = ByteBuffer.wrap(header); + inputStream.reset(); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(12); + if (syncword != 0xfff) { + return false; + } + brb.readBits(3); + int protectionAbsent = brb.readBits(1); + brb.readBits(14); + int frameSize = brb.readBits(13); + int bufferFullness = brb.readBits(11); + int noBlocks = brb.readBits(2); + int used = (int) Math.ceil(brb.getPosition() / 8.0); + if (protectionAbsent == 0) { + used += 2; + } + inputStream.skip(used); + frameSize -= used; +// System.out.println("Size: " + frameSize + " fullness: " + bufferFullness + " no blocks: " + noBlocks); + byte[] data = new byte[frameSize]; + inputStream.read(data); + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1024)); + inputStream.mark(15); + } + return ret; + } + + @Override + public String toString() { + return "AACTrackImpl{" + + "samplerate=" + samplerate + + ", bitrate=" + bitrate + + ", channelCount=" + channelCount + + ", channelconfig=" + channelconfig + + '}'; + } +} + diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AC3TrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AC3TrackImpl.java new file mode 100644 index 0000000..5e5b2cd --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AC3TrackImpl.java @@ -0,0 +1,513 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.AC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.BitReaderBuffer; + +import java.io.InputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +public class AC3TrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int channelCount; + + int fscod; + int bsid; + int bsmod; + int acmod; + int lfeon; + int frmsizecod; + + int frameSize; + int[][][][] bitRateAndFrameSizeTable; + + private InputStream inputStream; + private List<ByteBuffer> samples; + boolean readSamples = false; + List<TimeToSampleBox.Entry> stts; + private String lang = "und"; + + public AC3TrackImpl(InputStream fin, String lang) throws IOException { + this.lang = lang; + parse(fin); + } + + public AC3TrackImpl(InputStream fin) throws IOException { + parse(fin); + } + + private void parse(InputStream fin) throws IOException { + inputStream = fin; + bitRateAndFrameSizeTable = new int[19][2][3][2]; + stts = new LinkedList<TimeToSampleBox.Entry>(); + initBitRateAndFrameSizeTable(); + if (!readVariables()) { + throw new IOException(); + } + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("ac-3"); + audioSampleEntry.setChannelCount(2); // According to ETSI TS 102 366 Annex F + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + AC3SpecificBox ac3 = new AC3SpecificBox(); + ac3.setAcmod(acmod); + ac3.setBitRateCode(frmsizecod >> 1); + ac3.setBsid(bsid); + ac3.setBsmod(bsmod); + ac3.setFscod(fscod); + ac3.setLfeon(lfeon); + ac3.setReserved(0); + + audioSampleEntry.addBox(ac3); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + } + + + public List<ByteBuffer> getSamples() { + + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public Box getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() throws IOException { + byte[] data = new byte[100]; + inputStream.mark(100); + if (100 != inputStream.read(data, 0, 100)) { + return false; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(16); + if (syncword != 0xb77) { + return false; + } + brb.readBits(16); // CRC-1 + fscod = brb.readBits(2); + + switch (fscod) { + case 0: + samplerate = 48000; + break; + + case 1: + samplerate = 44100; + break; + + case 2: + samplerate = 32000; + break; + + case 3: + samplerate = 0; + break; + + } + if (samplerate == 0) { + return false; + } + + frmsizecod = brb.readBits(6); + + if (!calcBitrateAndFrameSize(frmsizecod)) { + return false; + } + + if (frameSize == 0) { + return false; + } + bsid = brb.readBits(5); + bsmod = brb.readBits(3); + acmod = brb.readBits(3); + + if (bsid == 9) { + samplerate /= 2; + } else if (bsid != 8 && bsid != 6) { + return false; + } + + if ((acmod != 1) && ((acmod & 1) == 1)) { + brb.readBits(2); + } + + if (0 != (acmod & 4)) { + brb.readBits(2); + } + + if (acmod == 2) { + brb.readBits(2); + } + + switch (acmod) { + case 0: + channelCount = 2; + break; + + case 1: + channelCount = 1; + break; + + case 2: + channelCount = 2; + break; + + case 3: + channelCount = 3; + break; + + case 4: + channelCount = 3; + break; + + case 5: + channelCount = 4; + break; + + case 6: + channelCount = 4; + break; + + case 7: + channelCount = 5; + break; + + } + + lfeon = brb.readBits(1); + + if (lfeon == 1) { + channelCount++; + } + return true; + } + + private boolean calcBitrateAndFrameSize(int code) { + int frmsizecode = code >>> 1; + int flag = code & 1; + if (frmsizecode > 18 || flag > 1 || fscod > 2) { + return false; + } + bitrate = bitRateAndFrameSizeTable[frmsizecode][flag][fscod][0]; + frameSize = 2 * bitRateAndFrameSizeTable[frmsizecode][flag][fscod][1]; + return true; + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + readSamples = true; + byte[] header = new byte[5]; + boolean ret = false; + inputStream.mark(5); + while (-1 != inputStream.read(header)) { + ret = true; + int frmsizecode = header[4] & 63; + calcBitrateAndFrameSize(frmsizecode); + inputStream.reset(); + byte[] data = new byte[frameSize]; + inputStream.read(data); + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1536)); + inputStream.mark(5); + } + return ret; + } + + private void initBitRateAndFrameSizeTable() { + // ETSI 102 366 Table 4.13, in frmsizecod, flag, fscod, bitrate/size order. Note that all sizes are in words, and all bitrates in kbps + + // 48kHz + bitRateAndFrameSizeTable[0][0][0][0] = 32; + bitRateAndFrameSizeTable[0][1][0][0] = 32; + bitRateAndFrameSizeTable[0][0][0][1] = 64; + bitRateAndFrameSizeTable[0][1][0][1] = 64; + bitRateAndFrameSizeTable[1][0][0][0] = 40; + bitRateAndFrameSizeTable[1][1][0][0] = 40; + bitRateAndFrameSizeTable[1][0][0][1] = 80; + bitRateAndFrameSizeTable[1][1][0][1] = 80; + bitRateAndFrameSizeTable[2][0][0][0] = 48; + bitRateAndFrameSizeTable[2][1][0][0] = 48; + bitRateAndFrameSizeTable[2][0][0][1] = 96; + bitRateAndFrameSizeTable[2][1][0][1] = 96; + bitRateAndFrameSizeTable[3][0][0][0] = 56; + bitRateAndFrameSizeTable[3][1][0][0] = 56; + bitRateAndFrameSizeTable[3][0][0][1] = 112; + bitRateAndFrameSizeTable[3][1][0][1] = 112; + bitRateAndFrameSizeTable[4][0][0][0] = 64; + bitRateAndFrameSizeTable[4][1][0][0] = 64; + bitRateAndFrameSizeTable[4][0][0][1] = 128; + bitRateAndFrameSizeTable[4][1][0][1] = 128; + bitRateAndFrameSizeTable[5][0][0][0] = 80; + bitRateAndFrameSizeTable[5][1][0][0] = 80; + bitRateAndFrameSizeTable[5][0][0][1] = 160; + bitRateAndFrameSizeTable[5][1][0][1] = 160; + bitRateAndFrameSizeTable[6][0][0][0] = 96; + bitRateAndFrameSizeTable[6][1][0][0] = 96; + bitRateAndFrameSizeTable[6][0][0][1] = 192; + bitRateAndFrameSizeTable[6][1][0][1] = 192; + bitRateAndFrameSizeTable[7][0][0][0] = 112; + bitRateAndFrameSizeTable[7][1][0][0] = 112; + bitRateAndFrameSizeTable[7][0][0][1] = 224; + bitRateAndFrameSizeTable[7][1][0][1] = 224; + bitRateAndFrameSizeTable[8][0][0][0] = 128; + bitRateAndFrameSizeTable[8][1][0][0] = 128; + bitRateAndFrameSizeTable[8][0][0][1] = 256; + bitRateAndFrameSizeTable[8][1][0][1] = 256; + bitRateAndFrameSizeTable[9][0][0][0] = 160; + bitRateAndFrameSizeTable[9][1][0][0] = 160; + bitRateAndFrameSizeTable[9][0][0][1] = 320; + bitRateAndFrameSizeTable[9][1][0][1] = 320; + bitRateAndFrameSizeTable[10][0][0][0] = 192; + bitRateAndFrameSizeTable[10][1][0][0] = 192; + bitRateAndFrameSizeTable[10][0][0][1] = 384; + bitRateAndFrameSizeTable[10][1][0][1] = 384; + bitRateAndFrameSizeTable[11][0][0][0] = 224; + bitRateAndFrameSizeTable[11][1][0][0] = 224; + bitRateAndFrameSizeTable[11][0][0][1] = 448; + bitRateAndFrameSizeTable[11][1][0][1] = 448; + bitRateAndFrameSizeTable[12][0][0][0] = 256; + bitRateAndFrameSizeTable[12][1][0][0] = 256; + bitRateAndFrameSizeTable[12][0][0][1] = 512; + bitRateAndFrameSizeTable[12][1][0][1] = 512; + bitRateAndFrameSizeTable[13][0][0][0] = 320; + bitRateAndFrameSizeTable[13][1][0][0] = 320; + bitRateAndFrameSizeTable[13][0][0][1] = 640; + bitRateAndFrameSizeTable[13][1][0][1] = 640; + bitRateAndFrameSizeTable[14][0][0][0] = 384; + bitRateAndFrameSizeTable[14][1][0][0] = 384; + bitRateAndFrameSizeTable[14][0][0][1] = 768; + bitRateAndFrameSizeTable[14][1][0][1] = 768; + bitRateAndFrameSizeTable[15][0][0][0] = 448; + bitRateAndFrameSizeTable[15][1][0][0] = 448; + bitRateAndFrameSizeTable[15][0][0][1] = 896; + bitRateAndFrameSizeTable[15][1][0][1] = 896; + bitRateAndFrameSizeTable[16][0][0][0] = 512; + bitRateAndFrameSizeTable[16][1][0][0] = 512; + bitRateAndFrameSizeTable[16][0][0][1] = 1024; + bitRateAndFrameSizeTable[16][1][0][1] = 1024; + bitRateAndFrameSizeTable[17][0][0][0] = 576; + bitRateAndFrameSizeTable[17][1][0][0] = 576; + bitRateAndFrameSizeTable[17][0][0][1] = 1152; + bitRateAndFrameSizeTable[17][1][0][1] = 1152; + bitRateAndFrameSizeTable[18][0][0][0] = 640; + bitRateAndFrameSizeTable[18][1][0][0] = 640; + bitRateAndFrameSizeTable[18][0][0][1] = 1280; + bitRateAndFrameSizeTable[18][1][0][1] = 1280; + + // 44.1 kHz + bitRateAndFrameSizeTable[0][0][1][0] = 32; + bitRateAndFrameSizeTable[0][1][1][0] = 32; + bitRateAndFrameSizeTable[0][0][1][1] = 69; + bitRateAndFrameSizeTable[0][1][1][1] = 70; + bitRateAndFrameSizeTable[1][0][1][0] = 40; + bitRateAndFrameSizeTable[1][1][1][0] = 40; + bitRateAndFrameSizeTable[1][0][1][1] = 87; + bitRateAndFrameSizeTable[1][1][1][1] = 88; + bitRateAndFrameSizeTable[2][0][1][0] = 48; + bitRateAndFrameSizeTable[2][1][1][0] = 48; + bitRateAndFrameSizeTable[2][0][1][1] = 104; + bitRateAndFrameSizeTable[2][1][1][1] = 105; + bitRateAndFrameSizeTable[3][0][1][0] = 56; + bitRateAndFrameSizeTable[3][1][1][0] = 56; + bitRateAndFrameSizeTable[3][0][1][1] = 121; + bitRateAndFrameSizeTable[3][1][1][1] = 122; + bitRateAndFrameSizeTable[4][0][1][0] = 64; + bitRateAndFrameSizeTable[4][1][1][0] = 64; + bitRateAndFrameSizeTable[4][0][1][1] = 139; + bitRateAndFrameSizeTable[4][1][1][1] = 140; + bitRateAndFrameSizeTable[5][0][1][0] = 80; + bitRateAndFrameSizeTable[5][1][1][0] = 80; + bitRateAndFrameSizeTable[5][0][1][1] = 174; + bitRateAndFrameSizeTable[5][1][1][1] = 175; + bitRateAndFrameSizeTable[6][0][1][0] = 96; + bitRateAndFrameSizeTable[6][1][1][0] = 96; + bitRateAndFrameSizeTable[6][0][1][1] = 208; + bitRateAndFrameSizeTable[6][1][1][1] = 209; + bitRateAndFrameSizeTable[7][0][1][0] = 112; + bitRateAndFrameSizeTable[7][1][1][0] = 112; + bitRateAndFrameSizeTable[7][0][1][1] = 243; + bitRateAndFrameSizeTable[7][1][1][1] = 244; + bitRateAndFrameSizeTable[8][0][1][0] = 128; + bitRateAndFrameSizeTable[8][1][1][0] = 128; + bitRateAndFrameSizeTable[8][0][1][1] = 278; + bitRateAndFrameSizeTable[8][1][1][1] = 279; + bitRateAndFrameSizeTable[9][0][1][0] = 160; + bitRateAndFrameSizeTable[9][1][1][0] = 160; + bitRateAndFrameSizeTable[9][0][1][1] = 348; + bitRateAndFrameSizeTable[9][1][1][1] = 349; + bitRateAndFrameSizeTable[10][0][1][0] = 192; + bitRateAndFrameSizeTable[10][1][1][0] = 192; + bitRateAndFrameSizeTable[10][0][1][1] = 417; + bitRateAndFrameSizeTable[10][1][1][1] = 418; + bitRateAndFrameSizeTable[11][0][1][0] = 224; + bitRateAndFrameSizeTable[11][1][1][0] = 224; + bitRateAndFrameSizeTable[11][0][1][1] = 487; + bitRateAndFrameSizeTable[11][1][1][1] = 488; + bitRateAndFrameSizeTable[12][0][1][0] = 256; + bitRateAndFrameSizeTable[12][1][1][0] = 256; + bitRateAndFrameSizeTable[12][0][1][1] = 557; + bitRateAndFrameSizeTable[12][1][1][1] = 558; + bitRateAndFrameSizeTable[13][0][1][0] = 320; + bitRateAndFrameSizeTable[13][1][1][0] = 320; + bitRateAndFrameSizeTable[13][0][1][1] = 696; + bitRateAndFrameSizeTable[13][1][1][1] = 697; + bitRateAndFrameSizeTable[14][0][1][0] = 384; + bitRateAndFrameSizeTable[14][1][1][0] = 384; + bitRateAndFrameSizeTable[14][0][1][1] = 835; + bitRateAndFrameSizeTable[14][1][1][1] = 836; + bitRateAndFrameSizeTable[15][0][1][0] = 448; + bitRateAndFrameSizeTable[15][1][1][0] = 448; + bitRateAndFrameSizeTable[15][0][1][1] = 975; + bitRateAndFrameSizeTable[15][1][1][1] = 975; + bitRateAndFrameSizeTable[16][0][1][0] = 512; + bitRateAndFrameSizeTable[16][1][1][0] = 512; + bitRateAndFrameSizeTable[16][0][1][1] = 1114; + bitRateAndFrameSizeTable[16][1][1][1] = 1115; + bitRateAndFrameSizeTable[17][0][1][0] = 576; + bitRateAndFrameSizeTable[17][1][1][0] = 576; + bitRateAndFrameSizeTable[17][0][1][1] = 1253; + bitRateAndFrameSizeTable[17][1][1][1] = 1254; + bitRateAndFrameSizeTable[18][0][1][0] = 640; + bitRateAndFrameSizeTable[18][1][1][0] = 640; + bitRateAndFrameSizeTable[18][0][1][1] = 1393; + bitRateAndFrameSizeTable[18][1][1][1] = 1394; + + // 32kHz + bitRateAndFrameSizeTable[0][0][2][0] = 32; + bitRateAndFrameSizeTable[0][1][2][0] = 32; + bitRateAndFrameSizeTable[0][0][2][1] = 96; + bitRateAndFrameSizeTable[0][1][2][1] = 96; + bitRateAndFrameSizeTable[1][0][2][0] = 40; + bitRateAndFrameSizeTable[1][1][2][0] = 40; + bitRateAndFrameSizeTable[1][0][2][1] = 120; + bitRateAndFrameSizeTable[1][1][2][1] = 120; + bitRateAndFrameSizeTable[2][0][2][0] = 48; + bitRateAndFrameSizeTable[2][1][2][0] = 48; + bitRateAndFrameSizeTable[2][0][2][1] = 144; + bitRateAndFrameSizeTable[2][1][2][1] = 144; + bitRateAndFrameSizeTable[3][0][2][0] = 56; + bitRateAndFrameSizeTable[3][1][2][0] = 56; + bitRateAndFrameSizeTable[3][0][2][1] = 168; + bitRateAndFrameSizeTable[3][1][2][1] = 168; + bitRateAndFrameSizeTable[4][0][2][0] = 64; + bitRateAndFrameSizeTable[4][1][2][0] = 64; + bitRateAndFrameSizeTable[4][0][2][1] = 192; + bitRateAndFrameSizeTable[4][1][2][1] = 192; + bitRateAndFrameSizeTable[5][0][2][0] = 80; + bitRateAndFrameSizeTable[5][1][2][0] = 80; + bitRateAndFrameSizeTable[5][0][2][1] = 240; + bitRateAndFrameSizeTable[5][1][2][1] = 240; + bitRateAndFrameSizeTable[6][0][2][0] = 96; + bitRateAndFrameSizeTable[6][1][2][0] = 96; + bitRateAndFrameSizeTable[6][0][2][1] = 288; + bitRateAndFrameSizeTable[6][1][2][1] = 288; + bitRateAndFrameSizeTable[7][0][2][0] = 112; + bitRateAndFrameSizeTable[7][1][2][0] = 112; + bitRateAndFrameSizeTable[7][0][2][1] = 336; + bitRateAndFrameSizeTable[7][1][2][1] = 336; + bitRateAndFrameSizeTable[8][0][2][0] = 128; + bitRateAndFrameSizeTable[8][1][2][0] = 128; + bitRateAndFrameSizeTable[8][0][2][1] = 384; + bitRateAndFrameSizeTable[8][1][2][1] = 384; + bitRateAndFrameSizeTable[9][0][2][0] = 160; + bitRateAndFrameSizeTable[9][1][2][0] = 160; + bitRateAndFrameSizeTable[9][0][2][1] = 480; + bitRateAndFrameSizeTable[9][1][2][1] = 480; + bitRateAndFrameSizeTable[10][0][2][0] = 192; + bitRateAndFrameSizeTable[10][1][2][0] = 192; + bitRateAndFrameSizeTable[10][0][2][1] = 576; + bitRateAndFrameSizeTable[10][1][2][1] = 576; + bitRateAndFrameSizeTable[11][0][2][0] = 224; + bitRateAndFrameSizeTable[11][1][2][0] = 224; + bitRateAndFrameSizeTable[11][0][2][1] = 672; + bitRateAndFrameSizeTable[11][1][2][1] = 672; + bitRateAndFrameSizeTable[12][0][2][0] = 256; + bitRateAndFrameSizeTable[12][1][2][0] = 256; + bitRateAndFrameSizeTable[12][0][2][1] = 768; + bitRateAndFrameSizeTable[12][1][2][1] = 768; + bitRateAndFrameSizeTable[13][0][2][0] = 320; + bitRateAndFrameSizeTable[13][1][2][0] = 320; + bitRateAndFrameSizeTable[13][0][2][1] = 960; + bitRateAndFrameSizeTable[13][1][2][1] = 960; + bitRateAndFrameSizeTable[14][0][2][0] = 384; + bitRateAndFrameSizeTable[14][1][2][0] = 384; + bitRateAndFrameSizeTable[14][0][2][1] = 1152; + bitRateAndFrameSizeTable[14][1][2][1] = 1152; + bitRateAndFrameSizeTable[15][0][2][0] = 448; + bitRateAndFrameSizeTable[15][1][2][0] = 448; + bitRateAndFrameSizeTable[15][0][2][1] = 1344; + bitRateAndFrameSizeTable[15][1][2][1] = 1344; + bitRateAndFrameSizeTable[16][0][2][0] = 512; + bitRateAndFrameSizeTable[16][1][2][0] = 512; + bitRateAndFrameSizeTable[16][0][2][1] = 1536; + bitRateAndFrameSizeTable[16][1][2][1] = 1536; + bitRateAndFrameSizeTable[17][0][2][0] = 576; + bitRateAndFrameSizeTable[17][1][2][0] = 576; + bitRateAndFrameSizeTable[17][0][2][1] = 1728; + bitRateAndFrameSizeTable[17][1][2][1] = 1728; + bitRateAndFrameSizeTable[18][0][2][0] = 640; + bitRateAndFrameSizeTable[18][1][2][0] = 640; + bitRateAndFrameSizeTable[18][0][2][1] = 1920; + bitRateAndFrameSizeTable[18][1][2][1] = 1920; + } +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/Amf0Track.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/Amf0Track.java new file mode 100644 index 0000000..0917767 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/Amf0Track.java @@ -0,0 +1,116 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.adobe.ActionMessageFormat0SampleEntryBox; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +public class Amf0Track extends AbstractTrack { + SortedMap<Long, byte[]> rawSamples = new TreeMap<Long, byte[]>() { + }; + private TrackMetaData trackMetaData = new TrackMetaData(); + + + /** + * Creates a new AMF0 track from + * + * @param rawSamples + */ + public Amf0Track(Map<Long, byte[]> rawSamples) { + this.rawSamples = new TreeMap<Long, byte[]>(rawSamples); + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); // Text tracks use millieseconds + trackMetaData.setLanguage("eng"); + } + + public List<ByteBuffer> getSamples() { + LinkedList<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + for (byte[] bytes : rawSamples.values()) { + samples.add(ByteBuffer.wrap(bytes)); + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + SampleDescriptionBox stsd = new SampleDescriptionBox(); + ActionMessageFormat0SampleEntryBox amf0 = new ActionMessageFormat0SampleEntryBox(); + amf0.setDataReferenceIndex(1); + stsd.addBox(amf0); + return stsd; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + LinkedList<TimeToSampleBox.Entry> timesToSample = new LinkedList<TimeToSampleBox.Entry>(); + LinkedList<Long> keys = new LinkedList<Long>(rawSamples.keySet()); + Collections.sort(keys); + long lastTimeStamp = 0; + for (Long key : keys) { + long delta = key - lastTimeStamp; + if (timesToSample.size() > 0 && timesToSample.peek().getDelta() == delta) { + timesToSample.peek().setCount(timesToSample.peek().getCount() + 1); + } else { + timesToSample.add(new TimeToSampleBox.Entry(1, delta)); + } + lastTimeStamp = key; + } + return timesToSample; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + // AMF0 tracks do not have Composition Time + return null; + } + + public long[] getSyncSamples() { + // AMF0 tracks do not have Sync Samples + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + // AMF0 tracks do not have Sample Dependencies + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; //To change body of implemented methods use File | Settings | File Templates. + } + + public String getHandler() { + return "data"; + } + + public Box getMediaHeaderBox() { + return new NullMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AppendTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AppendTrack.java new file mode 100644 index 0000000..93ee0cd --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/AppendTrack.java @@ -0,0 +1,348 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.*; + +/** + * Appends two or more <code>Tracks</code> of the same type. No only that the type must be equal + * also the decoder settings must be the same. + */ +public class AppendTrack extends AbstractTrack { + Track[] tracks; + SampleDescriptionBox stsd; + + public AppendTrack(Track... tracks) throws IOException { + this.tracks = tracks; + + for (Track track : tracks) { + + if (stsd == null) { + stsd = track.getSampleDescriptionBox(); + } else { + ByteArrayOutputStream curBaos = new ByteArrayOutputStream(); + ByteArrayOutputStream refBaos = new ByteArrayOutputStream(); + track.getSampleDescriptionBox().getBox(Channels.newChannel(curBaos)); + stsd.getBox(Channels.newChannel(refBaos)); + byte[] cur = curBaos.toByteArray(); + byte[] ref = refBaos.toByteArray(); + + if (!Arrays.equals(ref, cur)) { + SampleDescriptionBox curStsd = track.getSampleDescriptionBox(); + if (stsd.getBoxes().size() == 1 && curStsd.getBoxes().size() == 1) { + if (stsd.getBoxes().get(0) instanceof AudioSampleEntry && curStsd.getBoxes().get(0) instanceof AudioSampleEntry) { + AudioSampleEntry aseResult = mergeAudioSampleEntries((AudioSampleEntry) stsd.getBoxes().get(0), (AudioSampleEntry) curStsd.getBoxes().get(0)); + if (aseResult != null) { + stsd.setBoxes(Collections.<Box>singletonList(aseResult)); + return; + } + } + } + throw new IOException("Cannot append " + track + " to " + tracks[0] + " since their Sample Description Boxes differ: \n" + track.getSampleDescriptionBox() + "\n vs. \n" + tracks[0].getSampleDescriptionBox()); + } + } + } + } + + private AudioSampleEntry mergeAudioSampleEntries(AudioSampleEntry ase1, AudioSampleEntry ase2) throws IOException { + if (ase1.getType().equals(ase2.getType())) { + AudioSampleEntry ase = new AudioSampleEntry(ase2.getType()); + if (ase1.getBytesPerFrame() == ase2.getBytesPerFrame()) { + ase.setBytesPerFrame(ase1.getBytesPerFrame()); + } else { + return null; + } + if (ase1.getBytesPerPacket() == ase2.getBytesPerPacket()) { + ase.setBytesPerPacket(ase1.getBytesPerPacket()); + } else { + return null; + } + if (ase1.getBytesPerSample() == ase2.getBytesPerSample()) { + ase.setBytesPerSample(ase1.getBytesPerSample()); + } else { + return null; + } + if (ase1.getChannelCount() == ase2.getChannelCount()) { + ase.setChannelCount(ase1.getChannelCount()); + } else { + return null; + } + if (ase1.getPacketSize() == ase2.getPacketSize()) { + ase.setPacketSize(ase1.getPacketSize()); + } else { + return null; + } + if (ase1.getCompressionId() == ase2.getCompressionId()) { + ase.setCompressionId(ase1.getCompressionId()); + } else { + return null; + } + if (ase1.getSampleRate() == ase2.getSampleRate()) { + ase.setSampleRate(ase1.getSampleRate()); + } else { + return null; + } + if (ase1.getSampleSize() == ase2.getSampleSize()) { + ase.setSampleSize(ase1.getSampleSize()); + } else { + return null; + } + if (ase1.getSamplesPerPacket() == ase2.getSamplesPerPacket()) { + ase.setSamplesPerPacket(ase1.getSamplesPerPacket()); + } else { + return null; + } + if (ase1.getSoundVersion() == ase2.getSoundVersion()) { + ase.setSoundVersion(ase1.getSoundVersion()); + } else { + return null; + } + if (Arrays.equals(ase1.getSoundVersion2Data(), ase2.getSoundVersion2Data())) { + ase.setSoundVersion2Data(ase1.getSoundVersion2Data()); + } else { + return null; + } + if (ase1.getBoxes().size() == ase2.getBoxes().size()) { + Iterator<Box> bxs1 = ase1.getBoxes().iterator(); + Iterator<Box> bxs2 = ase2.getBoxes().iterator(); + while (bxs1.hasNext()) { + Box cur1 = bxs1.next(); + Box cur2 = bxs2.next(); + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + cur1.getBox(Channels.newChannel(baos1)); + cur2.getBox(Channels.newChannel(baos2)); + if (Arrays.equals(baos1.toByteArray(), baos2.toByteArray())) { + ase.addBox(cur1); + } else { + if (ESDescriptorBox.TYPE.equals(cur1.getType()) && ESDescriptorBox.TYPE.equals(cur2.getType())) { + ESDescriptorBox esdsBox1 = (ESDescriptorBox) cur1; + ESDescriptorBox esdsBox2 = (ESDescriptorBox) cur2; + ESDescriptor esds1 = esdsBox1.getEsDescriptor(); + ESDescriptor esds2 = esdsBox2.getEsDescriptor(); + if (esds1.getURLFlag() != esds2.getURLFlag()) { + return null; + } + if (esds1.getURLLength() != esds2.getURLLength()) { + return null; + } + if (esds1.getDependsOnEsId() != esds2.getDependsOnEsId()) { + return null; + } + if (esds1.getEsId() != esds2.getEsId()) { + return null; + } + if (esds1.getoCREsId() != esds2.getoCREsId()) { + return null; + } + if (esds1.getoCRstreamFlag() != esds2.getoCRstreamFlag()) { + return null; + } + if (esds1.getRemoteODFlag() != esds2.getRemoteODFlag()) { + return null; + } + if (esds1.getStreamDependenceFlag() != esds2.getStreamDependenceFlag()) { + return null; + } + if (esds1.getStreamPriority() != esds2.getStreamPriority()) { + return null; + } + if (esds1.getURLString() != null ? !esds1.getURLString().equals(esds2.getURLString()) : esds2.getURLString() != null) { + return null; + } + if (esds1.getDecoderConfigDescriptor() != null ? !esds1.getDecoderConfigDescriptor().equals(esds2.getDecoderConfigDescriptor()) : esds2.getDecoderConfigDescriptor() != null) { + DecoderConfigDescriptor dcd1 = esds1.getDecoderConfigDescriptor(); + DecoderConfigDescriptor dcd2 = esds2.getDecoderConfigDescriptor(); + if (!dcd1.getAudioSpecificInfo().equals(dcd2.getAudioSpecificInfo())) { + return null; + } + if (dcd1.getAvgBitRate() != dcd2.getAvgBitRate()) { + // I don't care + } + if (dcd1.getBufferSizeDB() != dcd2.getBufferSizeDB()) { + // I don't care + } + + if (dcd1.getDecoderSpecificInfo() != null ? !dcd1.getDecoderSpecificInfo().equals(dcd2.getDecoderSpecificInfo()) : dcd2.getDecoderSpecificInfo() != null) { + return null; + } + + if (dcd1.getMaxBitRate() != dcd2.getMaxBitRate()) { + // I don't care + } + if (!dcd1.getProfileLevelIndicationDescriptors().equals(dcd2.getProfileLevelIndicationDescriptors())) { + return null; + } + + if (dcd1.getObjectTypeIndication() != dcd2.getObjectTypeIndication()) { + return null; + } + if (dcd1.getStreamType() != dcd2.getStreamType()) { + return null; + } + if (dcd1.getUpStream() != dcd2.getUpStream()) { + return null; + } + + + } + if (esds1.getOtherDescriptors() != null ? !esds1.getOtherDescriptors().equals(esds2.getOtherDescriptors()) : esds2.getOtherDescriptors() != null) { + return null; + } + if (esds1.getSlConfigDescriptor() != null ? !esds1.getSlConfigDescriptor().equals(esds2.getSlConfigDescriptor()) : esds2.getSlConfigDescriptor() != null) { + return null; + } + ase.addBox(cur1); + } + } + } + } + return ase; + } else { + return null; + } + + + } + + + public List<ByteBuffer> getSamples() { + ArrayList<ByteBuffer> lists = new ArrayList<ByteBuffer>(); + + for (Track track : tracks) { + lists.addAll(track.getSamples()); + } + + return lists; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return stsd; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + if (tracks[0].getDecodingTimeEntries() != null && !tracks[0].getDecodingTimeEntries().isEmpty()) { + List<long[]> lists = new LinkedList<long[]>(); + for (Track track : tracks) { + lists.add(TimeToSampleBox.blowupTimeToSamples(track.getDecodingTimeEntries())); + } + + LinkedList<TimeToSampleBox.Entry> returnDecodingEntries = new LinkedList<TimeToSampleBox.Entry>(); + for (long[] list : lists) { + for (long nuDecodingTime : list) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getDelta() != nuDecodingTime) { + TimeToSampleBox.Entry e = new TimeToSampleBox.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + TimeToSampleBox.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + if (tracks[0].getCompositionTimeEntries() != null && !tracks[0].getCompositionTimeEntries().isEmpty()) { + List<int[]> lists = new LinkedList<int[]>(); + for (Track track : tracks) { + lists.add(CompositionTimeToSample.blowupCompositionTimes(track.getCompositionTimeEntries())); + } + LinkedList<CompositionTimeToSample.Entry> compositionTimeEntries = new LinkedList<CompositionTimeToSample.Entry>(); + for (int[] list : lists) { + for (int compositionTime : list) { + if (compositionTimeEntries.isEmpty() || compositionTimeEntries.getLast().getOffset() != compositionTime) { + CompositionTimeToSample.Entry e = new CompositionTimeToSample.Entry(1, compositionTime); + compositionTimeEntries.add(e); + } else { + CompositionTimeToSample.Entry e = compositionTimeEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + } + return compositionTimeEntries; + } else { + return null; + } + } + + public long[] getSyncSamples() { + if (tracks[0].getSyncSamples() != null && tracks[0].getSyncSamples().length > 0) { + int numSyncSamples = 0; + for (Track track : tracks) { + numSyncSamples += track.getSyncSamples().length; + } + long[] returnSyncSamples = new long[numSyncSamples]; + + int pos = 0; + long samplesBefore = 0; + for (Track track : tracks) { + for (long l : track.getSyncSamples()) { + returnSyncSamples[pos++] = samplesBefore + l; + } + samplesBefore += track.getSamples().size(); + } + return returnSyncSamples; + } else { + return null; + } + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + if (tracks[0].getSampleDependencies() != null && !tracks[0].getSampleDependencies().isEmpty()) { + List<SampleDependencyTypeBox.Entry> list = new LinkedList<SampleDependencyTypeBox.Entry>(); + for (Track track : tracks) { + list.addAll(track.getSampleDependencies()); + } + return list; + } else { + return null; + } + } + + public TrackMetaData getTrackMetaData() { + return tracks[0].getTrackMetaData(); + } + + public String getHandler() { + return tracks[0].getHandler(); + } + + public Box getMediaHeaderBox() { + return tracks[0].getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return tracks[0].getSubsampleInformationBox(); + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ChangeTimeScaleTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ChangeTimeScaleTrack.java new file mode 100644 index 0000000..50f76c2 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ChangeTimeScaleTrack.java @@ -0,0 +1,203 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.logging.Logger; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class ChangeTimeScaleTrack implements Track { + private static final Logger LOG = Logger.getLogger(ChangeTimeScaleTrack.class.getName()); + + Track source; + List<CompositionTimeToSample.Entry> ctts; + List<TimeToSampleBox.Entry> tts; + long timeScale; + + /** + * Changes the time scale of the source track to the target time scale and makes sure + * that any rounding errors that may have summed are corrected exactly before the syncSamples. + * + * @param source the source track + * @param targetTimeScale the resulting time scale of this track. + * @param syncSamples at these sync points where rounding error are corrected. + */ + public ChangeTimeScaleTrack(Track source, long targetTimeScale, long[] syncSamples) { + this.source = source; + this.timeScale = targetTimeScale; + double timeScaleFactor = (double) targetTimeScale / source.getTrackMetaData().getTimescale(); + ctts = adjustCtts(source.getCompositionTimeEntries(), timeScaleFactor); + tts = adjustTts(source.getDecodingTimeEntries(), timeScaleFactor, syncSamples, getTimes(source, syncSamples, targetTimeScale)); + } + + private static long[] getTimes(Track track, long[] syncSamples, long targetTimeScale) { + long[] syncSampleTimes = new long[syncSamples.length]; + Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); + + int currentSample = 1; // first syncsample is 1 + long currentDuration = 0; + long currentDelta = 0; + int currentSyncSampleIndex = 0; + long left = 0; + + + while (currentSample <= syncSamples[syncSamples.length - 1]) { + if (currentSample++ == syncSamples[currentSyncSampleIndex]) { + syncSampleTimes[currentSyncSampleIndex++] = (currentDuration * targetTimeScale) / track.getTrackMetaData().getTimescale(); + } + if (left-- == 0) { + TimeToSampleBox.Entry entry = timeQueue.poll(); + left = entry.getCount() - 1; + currentDelta = entry.getDelta(); + } + currentDuration += currentDelta; + } + return syncSampleTimes; + + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return tts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return ctts; + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(timeScale); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + /** + * Adjusting the composition times is easy. Just scale it by the factor - that's it. There is no rounding + * error summing up. + * + * @param source + * @param timeScaleFactor + * @return + */ + static List<CompositionTimeToSample.Entry> adjustCtts(List<CompositionTimeToSample.Entry> source, double timeScaleFactor) { + if (source != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(source.size()); + for (CompositionTimeToSample.Entry entry : source) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), (int) Math.round(timeScaleFactor * entry.getOffset()))); + } + return entries2; + } else { + return null; + } + } + + static List<TimeToSampleBox.Entry> adjustTts(List<TimeToSampleBox.Entry> source, double timeScaleFactor, long[] syncSample, long[] syncSampleTimes) { + + long[] sourceArray = TimeToSampleBox.blowupTimeToSamples(source); + long summedDurations = 0; + + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (int i = 1; i <= sourceArray.length; i++) { + long duration = sourceArray[i - 1]; + + long x = Math.round(timeScaleFactor * duration); + + + TimeToSampleBox.Entry last = entries2.peekLast(); + int ssIndex; + if ((ssIndex = Arrays.binarySearch(syncSample, i + 1)) >= 0) { + // we are at the sample before sync point + if (syncSampleTimes[ssIndex] != summedDurations) { + long correction = syncSampleTimes[ssIndex] - (summedDurations + x); + LOG.finest(String.format("Sample %d %d / %d - correct by %d", i, summedDurations, syncSampleTimes[ssIndex], correction)); + x += correction; + } + } + summedDurations += x; + if (last == null) { + entries2.add(new TimeToSampleBox.Entry(1, x)); + } else if (last.getDelta() != x) { + entries2.add(new TimeToSampleBox.Entry(1, x)); + } else { + last.setCount(last.getCount() + 1); + } + + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "ChangeTimeScaleTrack{" + + "source=" + source + + '}'; + } +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/CroppedTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/CroppedTrack.java new file mode 100644 index 0000000..2389961 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/CroppedTrack.java @@ -0,0 +1,151 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +/** + * Generates a Track that starts at fromSample and ends at toSample (exclusive). The user of this class + * has to make sure that the fromSample is a random access sample. + * <ul> + * <li>In AAC this is every single sample</li> + * <li>In H264 this is every sample that is marked in the SyncSampleBox</li> + * </ul> + */ +public class CroppedTrack extends AbstractTrack { + Track origTrack; + private int fromSample; + private int toSample; + private long[] syncSampleArray; + + public CroppedTrack(Track origTrack, long fromSample, long toSample) { + this.origTrack = origTrack; + assert fromSample <= Integer.MAX_VALUE; + assert toSample <= Integer.MAX_VALUE; + this.fromSample = (int) fromSample; + this.toSample = (int) toSample; + } + + public List<ByteBuffer> getSamples() { + return origTrack.getSamples().subList(fromSample, toSample); + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return origTrack.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + if (origTrack.getDecodingTimeEntries() != null && !origTrack.getDecodingTimeEntries().isEmpty()) { + // todo optimize! too much long is allocated but then not used + long[] decodingTimes = TimeToSampleBox.blowupTimeToSamples(origTrack.getDecodingTimeEntries()); + long[] nuDecodingTimes = new long[toSample - fromSample]; + System.arraycopy(decodingTimes, fromSample, nuDecodingTimes, 0, toSample - fromSample); + + LinkedList<TimeToSampleBox.Entry> returnDecodingEntries = new LinkedList<TimeToSampleBox.Entry>(); + + for (long nuDecodingTime : nuDecodingTimes) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getDelta() != nuDecodingTime) { + TimeToSampleBox.Entry e = new TimeToSampleBox.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + TimeToSampleBox.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + if (origTrack.getCompositionTimeEntries() != null && !origTrack.getCompositionTimeEntries().isEmpty()) { + int[] compositionTime = CompositionTimeToSample.blowupCompositionTimes(origTrack.getCompositionTimeEntries()); + int[] nuCompositionTimes = new int[toSample - fromSample]; + System.arraycopy(compositionTime, fromSample, nuCompositionTimes, 0, toSample - fromSample); + + LinkedList<CompositionTimeToSample.Entry> returnDecodingEntries = new LinkedList<CompositionTimeToSample.Entry>(); + + for (int nuDecodingTime : nuCompositionTimes) { + if (returnDecodingEntries.isEmpty() || returnDecodingEntries.getLast().getOffset() != nuDecodingTime) { + CompositionTimeToSample.Entry e = new CompositionTimeToSample.Entry(1, nuDecodingTime); + returnDecodingEntries.add(e); + } else { + CompositionTimeToSample.Entry e = returnDecodingEntries.getLast(); + e.setCount(e.getCount() + 1); + } + } + return returnDecodingEntries; + } else { + return null; + } + } + + synchronized public long[] getSyncSamples() { + if (this.syncSampleArray == null) { + if (origTrack.getSyncSamples() != null && origTrack.getSyncSamples().length > 0) { + List<Long> syncSamples = new LinkedList<Long>(); + for (long l : origTrack.getSyncSamples()) { + if (l >= fromSample && l < toSample) { + syncSamples.add(l - fromSample); + } + } + syncSampleArray = new long[syncSamples.size()]; + for (int i = 0; i < syncSampleArray.length; i++) { + syncSampleArray[i] = syncSamples.get(i); + + } + return syncSampleArray; + } else { + return null; + } + } else { + return this.syncSampleArray; + } + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + if (origTrack.getSampleDependencies() != null && !origTrack.getSampleDependencies().isEmpty()) { + return origTrack.getSampleDependencies().subList(fromSample, toSample); + } else { + return null; + } + } + + public TrackMetaData getTrackMetaData() { + return origTrack.getTrackMetaData(); + } + + public String getHandler() { + return origTrack.getHandler(); + } + + public Box getMediaHeaderBox() { + return origTrack.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return origTrack.getSubsampleInformationBox(); + } + +}
\ No newline at end of file diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/DivideTimeScaleTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/DivideTimeScaleTrack.java new file mode 100644 index 0000000..c51e8e0 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/DivideTimeScaleTrack.java @@ -0,0 +1,126 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class DivideTimeScaleTrack implements Track { + Track source; + private int timeScaleDivisor; + + public DivideTimeScaleTrack(Track source, int timeScaleDivisor) { + this.source = source; + this.timeScaleDivisor = timeScaleDivisor; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return adjustTts(); + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return adjustCtts(); + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(source.getTrackMetaData().getTimescale() / this.timeScaleDivisor); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + List<CompositionTimeToSample.Entry> adjustCtts() { + List<CompositionTimeToSample.Entry> origCtts = this.source.getCompositionTimeEntries(); + if (origCtts != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(origCtts.size()); + for (CompositionTimeToSample.Entry entry : origCtts) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), entry.getOffset() / timeScaleDivisor)); + } + return entries2; + } else { + return null; + } + } + + List<TimeToSampleBox.Entry> adjustTts() { + List<TimeToSampleBox.Entry> origTts = source.getDecodingTimeEntries(); + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (TimeToSampleBox.Entry e : origTts) { + entries2.add(new TimeToSampleBox.Entry(e.getCount(), e.getDelta() / timeScaleDivisor)); + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "MultiplyTimeScaleTrack{" + + "source=" + source + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/EC3TrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/EC3TrackImpl.java new file mode 100644 index 0000000..d0b2d76 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/EC3TrackImpl.java @@ -0,0 +1,436 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.EC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.BitReaderBuffer; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by IntelliJ IDEA. + * User: magnus + * Date: 2012-03-14 + * Time: 10:39 + * To change this template use File | Settings | File Templates. + */ +public class EC3TrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + int samplerate; + int bitrate; + int frameSize; + + List<BitStreamInfo> entries = new LinkedList<BitStreamInfo>(); + + private BufferedInputStream inputStream; + private List<ByteBuffer> samples; + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + private String lang = "und"; + + public EC3TrackImpl(InputStream fin, String lang) throws IOException { + this.lang = lang; + parse(fin); + } + + public EC3TrackImpl(InputStream fin) throws IOException { + parse(fin); + } + + private void parse(InputStream fin) throws IOException { + inputStream = new BufferedInputStream(fin); + + boolean done = false; + inputStream.mark(10000); + while (!done) { + BitStreamInfo bsi = readVariables(); + if (bsi == null) { + throw new IOException(); + } + for (BitStreamInfo entry : entries) { + if (bsi.strmtyp != 1 && entry.substreamid == bsi.substreamid) { + done = true; + } + } + if (!done) { + entries.add(bsi); + long skipped = inputStream.skip(bsi.frameSize); + assert skipped == bsi.frameSize; + } + } + + inputStream.reset(); + + if (entries.size() == 0) { + throw new IOException(); + } + samplerate = entries.get(0).samplerate; + + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("ec-3"); + audioSampleEntry.setChannelCount(2); // According to ETSI TS 102 366 Annex F + audioSampleEntry.setSampleRate(samplerate); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + EC3SpecificBox ec3 = new EC3SpecificBox(); + int[] deps = new int[entries.size()]; + int[] chan_locs = new int[entries.size()]; + for (BitStreamInfo bsi : entries) { + if (bsi.strmtyp == 1) { + deps[bsi.substreamid]++; + chan_locs[bsi.substreamid] = ((bsi.chanmap >> 6) & 0x100) | ((bsi.chanmap >> 5) & 0xff); + } + } + for (BitStreamInfo bsi : entries) { + if (bsi.strmtyp != 1) { + EC3SpecificBox.Entry e = new EC3SpecificBox.Entry(); + e.fscod = bsi.fscod; + e.bsid = bsi.bsid; + e.bsmod = bsi.bsmod; + e.acmod = bsi.acmod; + e.lfeon = bsi.lfeon; + e.reserved = 0; + e.num_dep_sub = deps[bsi.substreamid]; + e.chan_loc = chan_locs[bsi.substreamid]; + e.reserved2 = 0; + ec3.addEntry(e); + } + bitrate += bsi.bitrate; + frameSize += bsi.frameSize; + } + + ec3.setDataRate(bitrate / 1000); + audioSampleEntry.addBox(ec3); + sampleDescriptionBox.addBox(audioSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(samplerate); // Audio tracks always use samplerate as timescale + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + } + + + public List<ByteBuffer> getSamples() { + + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "soun"; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new SoundMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private BitStreamInfo readVariables() throws IOException { + byte[] data = new byte[200]; + inputStream.mark(200); + if (200 != inputStream.read(data, 0, 200)) { + return null; + } + inputStream.reset(); // Rewind + ByteBuffer bb = ByteBuffer.wrap(data); + BitReaderBuffer brb = new BitReaderBuffer(bb); + int syncword = brb.readBits(16); + if (syncword != 0xb77) { + return null; + } + + BitStreamInfo entry = new BitStreamInfo(); + + entry.strmtyp = brb.readBits(2); + entry.substreamid = brb.readBits(3); + int frmsiz = brb.readBits(11); + entry.frameSize = 2 * (frmsiz + 1); + + entry.fscod = brb.readBits(2); + int fscod2 = -1; + int numblkscod; + if (entry.fscod == 3) { + fscod2 = brb.readBits(2); + numblkscod = 3; + } else { + numblkscod = brb.readBits(2); + } + int numberOfBlocksPerSyncFrame = 0; + switch (numblkscod) { + case 0: + numberOfBlocksPerSyncFrame = 1; + break; + + case 1: + numberOfBlocksPerSyncFrame = 2; + break; + + case 2: + numberOfBlocksPerSyncFrame = 3; + break; + + case 3: + numberOfBlocksPerSyncFrame = 6; + break; + + } + entry.frameSize *= (6 / numberOfBlocksPerSyncFrame); + + entry.acmod = brb.readBits(3); + entry.lfeon = brb.readBits(1); + entry.bsid = brb.readBits(5); + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(8); // compr + } + if (0 == entry.acmod) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(8); + } + } + if (1 == entry.strmtyp) { + if (1 == brb.readBits(1)) { + entry.chanmap = brb.readBits(16); + } + } + if (1 == brb.readBits(1)) { // mixing metadata + if (entry.acmod > 2) { + brb.readBits(2); + } + if (1 == (entry.acmod & 1) && entry.acmod > 2) { + brb.readBits(3); + brb.readBits(3); + } + if (0 < (entry.acmod & 4)) { + brb.readBits(3); + brb.readBits(3); + } + if (1 == entry.lfeon) { + if (1 == brb.readBits(1)) { + brb.readBits(5); + } + } + if (0 == entry.strmtyp) { + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + if (0 == entry.acmod) { + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + } + if (1 == brb.readBits(1)) { + brb.readBits(6); + } + int mixdef = brb.readBits(2); + if (1 == mixdef) { + brb.readBits(5); + } else if (2 == mixdef) { + brb.readBits(12); + } else if (3 == mixdef) { + int mixdeflen = brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + if (1 == brb.readBits(1)) { + brb.readBits(4); + } + } + } + if (1 == brb.readBits(1)) { + brb.readBits(5); + if (1 == brb.readBits(1)) { + brb.readBits(7); + if (1 == brb.readBits(1)) { + brb.readBits(8); + } + } + } + for (int i = 0; i < (mixdeflen + 2); i++) { + brb.readBits(8); + } + brb.byteSync(); + } + if (entry.acmod < 2) { + if (1 == brb.readBits(1)) { + brb.readBits(14); + } + if (0 == entry.acmod) { + if (1 == brb.readBits(1)) { + brb.readBits(14); + } + } + if (1 == brb.readBits(1)) { + if (numblkscod == 0) { + brb.readBits(5); + } else { + for (int i = 0; i < numberOfBlocksPerSyncFrame; i++) { + if (1 == brb.readBits(1)) { + brb.readBits(5); + } + } + } + + } + } + } + } + if (1 == brb.readBits(1)) { // infomdate + entry.bsmod = brb.readBits(3); + } + + switch (entry.fscod) { + case 0: + entry.samplerate = 48000; + break; + + case 1: + entry.samplerate = 44100; + break; + + case 2: + entry.samplerate = 32000; + break; + + case 3: { + switch (fscod2) { + case 0: + entry.samplerate = 24000; + break; + + case 1: + entry.samplerate = 22050; + break; + + case 2: + entry.samplerate = 16000; + break; + + case 3: + entry.samplerate = 0; + break; + } + break; + } + + } + if (entry.samplerate == 0) { + return null; + } + + entry.bitrate = (int) (((double) entry.samplerate) / 1536.0 * entry.frameSize * 8); + + return entry; + } + + private boolean readSamples() throws IOException { + int read = frameSize; + boolean ret = false; + while (frameSize == read) { + ret = true; + byte[] data = new byte[frameSize]; + read = inputStream.read(data); + if (read == frameSize) { + samples.add(ByteBuffer.wrap(data)); + stts.add(new TimeToSampleBox.Entry(1, 1536)); + } + } + return ret; + } + + public static class BitStreamInfo extends EC3SpecificBox.Entry { + public int frameSize; + public int substreamid; + public int bitrate; + public int samplerate; + public int strmtyp; + public int chanmap; + + @Override + public String toString() { + return "BitStreamInfo{" + + "frameSize=" + frameSize + + ", substreamid=" + substreamid + + ", bitrate=" + bitrate + + ", samplerate=" + samplerate + + ", strmtyp=" + strmtyp + + ", chanmap=" + chanmap + + '}'; + } + } + + @Override + public String toString() { + return "EC3TrackImpl{" + + "bitrate=" + bitrate + + ", samplerate=" + samplerate + + ", entries=" + entries + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/H264TrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/H264TrackImpl.java new file mode 100644 index 0000000..b3c1866 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/H264TrackImpl.java @@ -0,0 +1,740 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.h264.model.PictureParameterSet; +import com.googlecode.mp4parser.h264.model.SeqParameterSet; +import com.googlecode.mp4parser.h264.read.CAVLCReader; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +/** + * The <code>H264TrackImpl</code> creates a <code>Track</code> from an H.264 + * Annex B file. + */ +public class H264TrackImpl extends AbstractTrack { + private static final Logger LOG = Logger.getLogger(H264TrackImpl.class.getName()); + + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + + private ReaderWrapper reader; + private List<ByteBuffer> samples; + boolean readSamples = false; + + List<TimeToSampleBox.Entry> stts; + List<CompositionTimeToSample.Entry> ctts; + List<SampleDependencyTypeBox.Entry> sdtp; + List<Integer> stss; + + SeqParameterSet seqParameterSet = null; + PictureParameterSet pictureParameterSet = null; + LinkedList<byte[]> seqParameterSetList = new LinkedList<byte[]>(); + LinkedList<byte[]> pictureParameterSetList = new LinkedList<byte[]>(); + + private int width; + private int height; + private int timescale; + private int frametick; + private int currentScSize; + private int prevScSize; + + private SEIMessage seiMessage; + int frameNrInGop = 0; + private boolean determineFrameRate = true; + private String lang = "und"; + + public H264TrackImpl(InputStream inputStream, String lang, long timescale) throws IOException { + this.lang = lang; + if (timescale > 1000) { + timescale = timescale; //e.g. 23976 + frametick = 1000; + determineFrameRate = false; + } else { + throw new IllegalArgumentException("Timescale must be specified in milliseconds!"); + } + parse(inputStream); + } + + public H264TrackImpl(InputStream inputStream, String lang) throws IOException { + this.lang = lang; + parse(inputStream); + } + + public H264TrackImpl(InputStream inputStream) throws IOException { + parse(inputStream); + } + + private void parse(InputStream inputStream) throws IOException { + this.reader = new ReaderWrapper(inputStream); + stts = new LinkedList<TimeToSampleBox.Entry>(); + ctts = new LinkedList<CompositionTimeToSample.Entry>(); + sdtp = new LinkedList<SampleDependencyTypeBox.Entry>(); + stss = new LinkedList<Integer>(); + + samples = new LinkedList<ByteBuffer>(); + if (!readSamples()) { + throw new IOException(); + } + + if (!readVariables()) { + throw new IOException(); + } + + sampleDescriptionBox = new SampleDescriptionBox(); + VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1"); + visualSampleEntry.setDataReferenceIndex(1); + visualSampleEntry.setDepth(24); + visualSampleEntry.setFrameCount(1); + visualSampleEntry.setHorizresolution(72); + visualSampleEntry.setVertresolution(72); + visualSampleEntry.setWidth(width); + visualSampleEntry.setHeight(height); + visualSampleEntry.setCompressorname("AVC Coding"); + + AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox(); + + avcConfigurationBox.setSequenceParameterSets(seqParameterSetList); + avcConfigurationBox.setPictureParameterSets(pictureParameterSetList); + avcConfigurationBox.setAvcLevelIndication(seqParameterSet.level_idc); + avcConfigurationBox.setAvcProfileIndication(seqParameterSet.profile_idc); + avcConfigurationBox.setBitDepthLumaMinus8(seqParameterSet.bit_depth_luma_minus8); + avcConfigurationBox.setBitDepthChromaMinus8(seqParameterSet.bit_depth_chroma_minus8); + avcConfigurationBox.setChromaFormat(seqParameterSet.chroma_format_idc.getId()); + avcConfigurationBox.setConfigurationVersion(1); + avcConfigurationBox.setLengthSizeMinusOne(3); + avcConfigurationBox.setProfileCompatibility(seqParameterSetList.get(0)[1]); + + visualSampleEntry.addBox(avcConfigurationBox); + sampleDescriptionBox.addBox(visualSampleEntry); + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setLanguage(lang); + trackMetaData.setTimescale(timescale); + trackMetaData.setWidth(width); + trackMetaData.setHeight(height); + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return ctts; + } + + public long[] getSyncSamples() { + long[] returns = new long[stss.size()]; + for (int i = 0; i < stss.size(); i++) { + returns[i] = stss.get(i); + } + return returns; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return sdtp; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "vide"; + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new VideoMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + private boolean readVariables() { + width = (seqParameterSet.pic_width_in_mbs_minus1 + 1) * 16; + int mult = 2; + if (seqParameterSet.frame_mbs_only_flag) { + mult = 1; + } + height = 16 * (seqParameterSet.pic_height_in_map_units_minus1 + 1) * mult; + if (seqParameterSet.frame_cropping_flag) { + int chromaArrayType = 0; + if (seqParameterSet.residual_color_transform_flag == false) { + chromaArrayType = seqParameterSet.chroma_format_idc.getId(); + } + int cropUnitX = 1; + int cropUnitY = mult; + if (chromaArrayType != 0) { + cropUnitX = seqParameterSet.chroma_format_idc.getSubWidth(); + cropUnitY = seqParameterSet.chroma_format_idc.getSubHeight() * mult; + } + + width -= cropUnitX * (seqParameterSet.frame_crop_left_offset + seqParameterSet.frame_crop_right_offset); + height -= cropUnitY * (seqParameterSet.frame_crop_top_offset + seqParameterSet.frame_crop_bottom_offset); + } + return true; + } + + private boolean findNextStartcode() throws IOException { + byte[] test = new byte[]{-1, -1, -1, -1}; + + int c; + while ((c = reader.read()) != -1) { + test[0] = test[1]; + test[1] = test[2]; + test[2] = test[3]; + test[3] = (byte) c; + if (test[0] == 0 && test[1] == 0 && test[2] == 0 && test[3] == 1) { + prevScSize = currentScSize; + currentScSize = 4; + return true; + } + if (test[0] == 0 && test[1] == 0 && test[2] == 1) { + prevScSize = currentScSize; + currentScSize = 3; + return true; + } + } + return false; + } + + private enum NALActions { + IGNORE, BUFFER, STORE, END + } + + private boolean readSamples() throws IOException { + if (readSamples) { + return true; + } + + readSamples = true; + + + findNextStartcode(); + reader.mark(); + long pos = reader.getPos(); + + ArrayList<byte[]> buffered = new ArrayList<byte[]>(); + + int frameNr = 0; + + while (findNextStartcode()) { + long newpos = reader.getPos(); + int size = (int) (newpos - pos - prevScSize); + reader.reset(); + byte[] data = new byte[size ]; + reader.read(data); + int type = data[0]; + int nal_ref_idc = (type >> 5) & 3; + int nal_unit_type = type & 0x1f; + LOG.fine("Found startcode at " + (pos -4) + " Type: " + nal_unit_type + " ref idc: " + nal_ref_idc + " (size " + size + ")"); + NALActions action = handleNALUnit(nal_ref_idc, nal_unit_type, data); + switch (action) { + case IGNORE: + break; + + case BUFFER: + buffered.add(data); + break; + + case STORE: + int stdpValue = 22; + frameNr++; + buffered.add(data); + ByteBuffer bb = createSample(buffered); + boolean IdrPicFlag = false; + if (nal_unit_type == 5) { + stdpValue += 16; + IdrPicFlag = true; + } + ByteArrayInputStream bs = cleanBuffer(buffered.get(buffered.size() - 1)); + SliceHeader sh = new SliceHeader(bs, seqParameterSet, pictureParameterSet, IdrPicFlag); + if (sh.slice_type == SliceHeader.SliceType.B) { + stdpValue += 4; + } + LOG.fine("Adding sample with size " + bb.capacity() + " and header " + sh); + buffered.clear(); + samples.add(bb); + stts.add(new TimeToSampleBox.Entry(1, frametick)); + if (nal_unit_type == 5) { // IDR Picture + stss.add(frameNr); + } + if (seiMessage.n_frames == 0) { + frameNrInGop = 0; + } + int offset = 0; + if (seiMessage.clock_timestamp_flag) { + offset = seiMessage.n_frames - frameNrInGop; + } else if (seiMessage.removal_delay_flag) { + offset = seiMessage.dpb_removal_delay / 2; + } + ctts.add(new CompositionTimeToSample.Entry(1, offset * frametick)); + sdtp.add(new SampleDependencyTypeBox.Entry(stdpValue)); + frameNrInGop++; + break; + + case END: + return true; + + + } + pos = newpos; + reader.seek(currentScSize); + reader.mark(); + } + return true; + } + + private ByteBuffer createSample(List<byte[]> buffers) { + int outsize = 0; + for (int i = 0; i < buffers.size(); i++) { + outsize += buffers.get(i).length + 4; + } + byte[] output = new byte[outsize]; + + ByteBuffer bb = ByteBuffer.wrap(output); + for (int i = 0; i < buffers.size(); i++) { + bb.putInt(buffers.get(i).length); + bb.put(buffers.get(i)); + } + bb.rewind(); + return bb; + } + + private ByteArrayInputStream cleanBuffer(byte[] data) { + byte[] output = new byte[data.length]; + int inPos = 0; + int outPos = 0; + while (inPos < data.length) { + if (data[inPos] == 0 && data[inPos + 1] == 0 && data[inPos + 2] == 3) { + output[outPos] = 0; + output[outPos + 1] = 0; + inPos += 3; + outPos += 2; + } else { + output[outPos] = data[inPos]; + inPos++; + outPos++; + } + } + return new ByteArrayInputStream(output, 0, outPos); + } + + private NALActions handleNALUnit(int nal_ref_idc, int nal_unit_type, byte[] data) throws IOException { + NALActions action; + switch (nal_unit_type) { + case 1: + case 2: + case 3: + case 4: + case 5: + action = NALActions.STORE; // Will only work in single slice per frame mode! + break; + + case 6: + seiMessage = new SEIMessage(cleanBuffer(data), seqParameterSet); + action = NALActions.BUFFER; + break; + + case 9: +// printAccessUnitDelimiter(data); + int type = data[1] >> 5; + LOG.fine("Access unit delimiter type: " + type); + action = NALActions.BUFFER; + break; + + + case 7: + if (seqParameterSet == null) { + ByteArrayInputStream is = cleanBuffer(data); + is.read(); + seqParameterSet = SeqParameterSet.read(is); + seqParameterSetList.add(data); + configureFramerate(); + } + action = NALActions.IGNORE; + break; + + case 8: + if (pictureParameterSet == null) { + ByteArrayInputStream is = new ByteArrayInputStream(data); + is.read(); + pictureParameterSet = PictureParameterSet.read(is); + pictureParameterSetList.add(data); + } + action = NALActions.IGNORE; + break; + + case 10: + case 11: + action = NALActions.END; + break; + + default: + System.err.println("Unknown NAL unit type: " + nal_unit_type); + action = NALActions.IGNORE; + + } + + return action; + } + + private void configureFramerate() { + if (determineFrameRate) { + if (seqParameterSet.vuiParams != null) { + timescale = seqParameterSet.vuiParams.time_scale >> 1; // Not sure why, but I found this in several places, and it works... + frametick = seqParameterSet.vuiParams.num_units_in_tick; + if (timescale == 0 || frametick == 0) { + System.err.println("Warning: vuiParams contain invalid values: time_scale: " + timescale + " and frame_tick: " + frametick + ". Setting frame rate to 25fps"); + timescale = 90000; + frametick = 3600; + } + } else { + System.err.println("Warning: Can't determine frame rate. Guessing 25 fps"); + timescale = 90000; + frametick = 3600; + } + } + } + + public void printAccessUnitDelimiter(byte[] data) { + LOG.fine("Access unit delimiter: " + (data[1] >> 5)); + } + + public static class SliceHeader { + + public enum SliceType { + P, B, I, SP, SI + } + + public int first_mb_in_slice; + public SliceType slice_type; + public int pic_parameter_set_id; + public int colour_plane_id; + public int frame_num; + public boolean field_pic_flag = false; + public boolean bottom_field_flag = false; + public int idr_pic_id; + public int pic_order_cnt_lsb; + public int delta_pic_order_cnt_bottom; + + public SliceHeader(InputStream is, SeqParameterSet sps, PictureParameterSet pps, boolean IdrPicFlag) throws IOException { + is.read(); + CAVLCReader reader = new CAVLCReader(is); + first_mb_in_slice = reader.readUE("SliceHeader: first_mb_in_slice"); + switch (reader.readUE("SliceHeader: slice_type")) { + case 0: + case 5: + slice_type = SliceType.P; + break; + + case 1: + case 6: + slice_type = SliceType.B; + break; + + case 2: + case 7: + slice_type = SliceType.I; + break; + + case 3: + case 8: + slice_type = SliceType.SP; + break; + + case 4: + case 9: + slice_type = SliceType.SI; + break; + + } + pic_parameter_set_id = reader.readUE("SliceHeader: pic_parameter_set_id"); + if (sps.residual_color_transform_flag) { + colour_plane_id = reader.readU(2, "SliceHeader: colour_plane_id"); + } + frame_num = reader.readU(sps.log2_max_frame_num_minus4 + 4, "SliceHeader: frame_num"); + + if (!sps.frame_mbs_only_flag) { + field_pic_flag = reader.readBool("SliceHeader: field_pic_flag"); + if (field_pic_flag) { + bottom_field_flag = reader.readBool("SliceHeader: bottom_field_flag"); + } + } + if (IdrPicFlag) { + idr_pic_id = reader.readUE("SliceHeader: idr_pic_id"); + if (sps.pic_order_cnt_type == 0) { + pic_order_cnt_lsb = reader.readU(sps.log2_max_pic_order_cnt_lsb_minus4 + 4, "SliceHeader: pic_order_cnt_lsb"); + if (pps.pic_order_present_flag && !field_pic_flag) { + delta_pic_order_cnt_bottom = reader.readSE("SliceHeader: delta_pic_order_cnt_bottom"); + } + } + } + } + + @Override + public String toString() { + return "SliceHeader{" + + "first_mb_in_slice=" + first_mb_in_slice + + ", slice_type=" + slice_type + + ", pic_parameter_set_id=" + pic_parameter_set_id + + ", colour_plane_id=" + colour_plane_id + + ", frame_num=" + frame_num + + ", field_pic_flag=" + field_pic_flag + + ", bottom_field_flag=" + bottom_field_flag + + ", idr_pic_id=" + idr_pic_id + + ", pic_order_cnt_lsb=" + pic_order_cnt_lsb + + ", delta_pic_order_cnt_bottom=" + delta_pic_order_cnt_bottom + + '}'; + } + } + + private class ReaderWrapper { + private InputStream inputStream; + private long pos = 0; + + private long markPos = 0; + + + private ReaderWrapper(InputStream inputStream) { + this.inputStream = inputStream; + } + + int read() throws IOException { + pos++; + return inputStream.read(); + } + + long read(byte[] data) throws IOException { + long read = inputStream.read(data); + pos += read; + return read; + } + + long seek(int dist) throws IOException { + long seeked = inputStream.skip(dist); + pos += seeked; + return seeked; + } + + public long getPos() { + return pos; + } + + public void mark() { + int i = 1048576; + LOG.fine("Marking with " + i + " at " + pos); + inputStream.mark(i); + markPos = pos; + } + + + public void reset() throws IOException { + long diff = pos - markPos; + LOG.fine("Resetting to " + markPos + " (pos is " + pos + ") which makes the buffersize " + diff); + inputStream.reset(); + pos = markPos; + } + } + + public class SEIMessage { + + int payloadType = 0; + int payloadSize = 0; + + boolean removal_delay_flag; + int cpb_removal_delay; + int dpb_removal_delay; + + boolean clock_timestamp_flag; + int pic_struct; + int ct_type; + int nuit_field_based_flag; + int counting_type; + int full_timestamp_flag; + int discontinuity_flag; + int cnt_dropped_flag; + int n_frames; + int seconds_value; + int minutes_value; + int hours_value; + int time_offset_length; + int time_offset; + + SeqParameterSet sps; + + public SEIMessage(InputStream is, SeqParameterSet sps) throws IOException { + this.sps = sps; + is.read(); + int datasize = is.available(); + int read = 0; + while (read < datasize) { + payloadType = 0; + payloadSize = 0; + int last_payload_type_bytes = is.read(); + read++; + while (last_payload_type_bytes == 0xff) { + payloadType += last_payload_type_bytes; + last_payload_type_bytes = is.read(); + read++; + } + payloadType += last_payload_type_bytes; + int last_payload_size_bytes = is.read(); + read++; + + while (last_payload_size_bytes == 0xff) { + payloadSize += last_payload_size_bytes; + last_payload_size_bytes = is.read(); + read++; + } + payloadSize += last_payload_size_bytes; + if (datasize - read >= payloadSize) { + if (payloadType == 1) { // pic_timing is what we are interested in! + if (sps.vuiParams != null && (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null || sps.vuiParams.pic_struct_present_flag)) { + byte[] data = new byte[payloadSize]; + is.read(data); + read += payloadSize; + CAVLCReader reader = new CAVLCReader(new ByteArrayInputStream(data)); + if (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null) { + removal_delay_flag = true; + cpb_removal_delay = reader.readU(sps.vuiParams.nalHRDParams.cpb_removal_delay_length_minus1 + 1, "SEI: cpb_removal_delay"); + dpb_removal_delay = reader.readU(sps.vuiParams.nalHRDParams.dpb_output_delay_length_minus1 + 1, "SEI: dpb_removal_delay"); + } else { + removal_delay_flag = false; + } + if (sps.vuiParams.pic_struct_present_flag) { + pic_struct = reader.readU(4, "SEI: pic_struct"); + int numClockTS; + switch (pic_struct) { + case 0: + case 1: + case 2: + default: + numClockTS = 1; + break; + + case 3: + case 4: + case 7: + numClockTS = 2; + break; + + case 5: + case 6: + case 8: + numClockTS = 3; + break; + } + for (int i = 0; i < numClockTS; i++) { + clock_timestamp_flag = reader.readBool("pic_timing SEI: clock_timestamp_flag[" + i + "]"); + if (clock_timestamp_flag) { + ct_type = reader.readU(2, "pic_timing SEI: ct_type"); + nuit_field_based_flag = reader.readU(1, "pic_timing SEI: nuit_field_based_flag"); + counting_type = reader.readU(5, "pic_timing SEI: counting_type"); + full_timestamp_flag = reader.readU(1, "pic_timing SEI: full_timestamp_flag"); + discontinuity_flag = reader.readU(1, "pic_timing SEI: discontinuity_flag"); + cnt_dropped_flag = reader.readU(1, "pic_timing SEI: cnt_dropped_flag"); + n_frames = reader.readU(8, "pic_timing SEI: n_frames"); + if (full_timestamp_flag == 1) { + seconds_value = reader.readU(6, "pic_timing SEI: seconds_value"); + minutes_value = reader.readU(6, "pic_timing SEI: minutes_value"); + hours_value = reader.readU(5, "pic_timing SEI: hours_value"); + } else { + if (reader.readBool("pic_timing SEI: seconds_flag")) { + seconds_value = reader.readU(6, "pic_timing SEI: seconds_value"); + if (reader.readBool("pic_timing SEI: minutes_flag")) { + minutes_value = reader.readU(6, "pic_timing SEI: minutes_value"); + if (reader.readBool("pic_timing SEI: hours_flag")) { + hours_value = reader.readU(5, "pic_timing SEI: hours_value"); + } + } + } + } + if (true) { + if (sps.vuiParams.nalHRDParams != null) { + time_offset_length = sps.vuiParams.nalHRDParams.time_offset_length; + } else if (sps.vuiParams.vclHRDParams != null) { + time_offset_length = sps.vuiParams.vclHRDParams.time_offset_length; + } else { + time_offset_length = 24; + } + time_offset = reader.readU(24, "pic_timing SEI: time_offset"); + } + } + } + } + + } else { + for (int i = 0; i < payloadSize; i++) { + is.read(); + read++; + } + } + } else { + for (int i = 0; i < payloadSize; i++) { + is.read(); + read++; + } + } + } else { + read = datasize; + } + LOG.fine(this.toString()); + } + } + + @Override + public String toString() { + String out = "SEIMessage{" + + "payloadType=" + payloadType + + ", payloadSize=" + payloadSize; + if (payloadType == 1) { + if (sps.vuiParams.nalHRDParams != null || sps.vuiParams.vclHRDParams != null) { + + out += ", cpb_removal_delay=" + cpb_removal_delay + + ", dpb_removal_delay=" + dpb_removal_delay; + } + if (sps.vuiParams.pic_struct_present_flag) { + out += ", pic_struct=" + pic_struct; + if (clock_timestamp_flag) { + out += ", ct_type=" + ct_type + + ", nuit_field_based_flag=" + nuit_field_based_flag + + ", counting_type=" + counting_type + + ", full_timestamp_flag=" + full_timestamp_flag + + ", discontinuity_flag=" + discontinuity_flag + + ", cnt_dropped_flag=" + cnt_dropped_flag + + ", n_frames=" + n_frames + + ", seconds_value=" + seconds_value + + ", minutes_value=" + minutes_value + + ", hours_value=" + hours_value + + ", time_offset_length=" + time_offset_length + + ", time_offset=" + time_offset; + } + } + } + out += '}'; + return out; + } + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/MultiplyTimeScaleTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/MultiplyTimeScaleTrack.java new file mode 100644 index 0000000..e9a90e4 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/MultiplyTimeScaleTrack.java @@ -0,0 +1,130 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; +import static com.googlecode.mp4parser.util.Math.gcd; +import static com.googlecode.mp4parser.util.Math.lcm; +import static java.lang.Math.round; + +/** + * Changes the timescale of a track by wrapping the track. + */ +public class MultiplyTimeScaleTrack implements Track { + Track source; + private int timeScaleFactor; + + public MultiplyTimeScaleTrack(Track source, int timeScaleFactor) { + this.source = source; + this.timeScaleFactor = timeScaleFactor; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return adjustTts(source.getDecodingTimeEntries(), timeScaleFactor); + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return adjustCtts(source.getCompositionTimeEntries(), timeScaleFactor); + } + + public long[] getSyncSamples() { + return source.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return source.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone(); + trackMetaData.setTimescale(source.getTrackMetaData().getTimescale() * this.timeScaleFactor); + return trackMetaData; + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return source.getSamples(); + } + + + static List<CompositionTimeToSample.Entry> adjustCtts(List<CompositionTimeToSample.Entry> source, int timeScaleFactor) { + if (source != null) { + List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(source.size()); + for (CompositionTimeToSample.Entry entry : source) { + entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), timeScaleFactor * entry.getOffset())); + } + return entries2; + } else { + return null; + } + } + + static List<TimeToSampleBox.Entry> adjustTts(List<TimeToSampleBox.Entry> source, int timeScaleFactor) { + LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>(); + for (TimeToSampleBox.Entry e : source) { + entries2.add(new TimeToSampleBox.Entry(e.getCount(), timeScaleFactor * e.getDelta())); + } + return entries2; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return source.getSubsampleInformationBox(); + } + + @Override + public String toString() { + return "MultiplyTimeScaleTrack{" + + "source=" + source + + '}'; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/QuicktimeTextTrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/QuicktimeTextTrackImpl.java new file mode 100644 index 0000000..8efa399 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/QuicktimeTextTrackImpl.java @@ -0,0 +1,165 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.TextSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.apple.BaseMediaInfoAtom; +import com.googlecode.mp4parser.boxes.apple.GenericMediaHeaderAtom; +import com.googlecode.mp4parser.boxes.apple.GenericMediaHeaderTextAtom; +import com.googlecode.mp4parser.boxes.apple.QuicktimeTextSampleEntry; +import com.googlecode.mp4parser.boxes.threegpp26245.FontTableBox; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * A Text track as Quicktime Pro would create. + */ +public class QuicktimeTextTrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + List<Line> subs = new LinkedList<Line>(); + + public List<Line> getSubs() { + return subs; + } + + public QuicktimeTextTrackImpl() { + sampleDescriptionBox = new SampleDescriptionBox(); + QuicktimeTextSampleEntry textTrack = new QuicktimeTextSampleEntry(); + textTrack.setDataReferenceIndex(1); + sampleDescriptionBox.addBox(textTrack); + + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); + + + } + + + public List<ByteBuffer> getSamples() { + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + samples.add(ByteBuffer.wrap(new byte[]{0, 0})); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + dos.writeShort(sub.text.getBytes("UTF-8").length); + dos.write(sub.text.getBytes("UTF-8")); + dos.close(); + } catch (IOException e) { + throw new Error("VM is broken. Does not support UTF-8"); + } + samples.add(ByteBuffer.wrap(baos.toByteArray())); + lastEnd = sub.to; + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + stts.add(new TimeToSampleBox.Entry(1, silentTime)); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + stts.add(new TimeToSampleBox.Entry(1, sub.to - sub.from)); + lastEnd = sub.to; + } + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "text"; + } + + + public static class Line { + long from; + long to; + String text; + + + public Line(long from, long to, String text) { + this.from = from; + this.to = to; + this.text = text; + } + + public long getFrom() { + return from; + } + + public String getText() { + return text; + } + + public long getTo() { + return to; + } + } + + public Box getMediaHeaderBox() { + GenericMediaHeaderAtom ghmd = new GenericMediaHeaderAtom(); + ghmd.addBox(new BaseMediaInfoAtom()); + ghmd.addBox(new GenericMediaHeaderTextAtom()); + return ghmd; + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ReplaceSampleTrack.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ReplaceSampleTrack.java new file mode 100644 index 0000000..81a129d --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/ReplaceSampleTrack.java @@ -0,0 +1,104 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.AbstractList; +import java.util.LinkedList; +import java.util.List; + +/** + * Generates a Track where a single sample has been replaced by a given <code>ByteBuffer</code>. + */ + +public class ReplaceSampleTrack extends AbstractTrack { + Track origTrack; + private long sampleNumber; + private ByteBuffer sampleContent; + private List<ByteBuffer> samples; + + public ReplaceSampleTrack(Track origTrack, long sampleNumber, ByteBuffer content) { + this.origTrack = origTrack; + this.sampleNumber = sampleNumber; + this.sampleContent = content; + this.samples = new ReplaceASingleEntryList(); + + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return origTrack.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return origTrack.getDecodingTimeEntries(); + + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return origTrack.getCompositionTimeEntries(); + + } + + synchronized public long[] getSyncSamples() { + return origTrack.getSyncSamples(); + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return origTrack.getSampleDependencies(); + } + + public TrackMetaData getTrackMetaData() { + return origTrack.getTrackMetaData(); + } + + public String getHandler() { + return origTrack.getHandler(); + } + + public Box getMediaHeaderBox() { + return origTrack.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return origTrack.getSubsampleInformationBox(); + } + + private class ReplaceASingleEntryList extends AbstractList<ByteBuffer> { + @Override + public ByteBuffer get(int index) { + if (ReplaceSampleTrack.this.sampleNumber == index) { + return ReplaceSampleTrack.this.sampleContent; + } else { + return ReplaceSampleTrack.this.origTrack.getSamples().get(index); + } + } + + @Override + public int size() { + return ReplaceSampleTrack.this.origTrack.getSamples().size(); + } + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/SilenceTrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/SilenceTrackImpl.java new file mode 100644 index 0000000..f74ab3c --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/SilenceTrackImpl.java @@ -0,0 +1,98 @@ +package com.googlecode.mp4parser.authoring.tracks; + +import com.coremedia.iso.boxes.*; +import com.googlecode.mp4parser.authoring.Mp4TrackImpl; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.TrackMetaData; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * This is just a basic idea how things could work but they don't. + */ +public class SilenceTrackImpl implements Track { + Track source; + + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + TimeToSampleBox.Entry entry; + + public SilenceTrackImpl(Track ofType, long ms) { + source = ofType; + if ("mp4a".equals(ofType.getSampleDescriptionBox().getSampleEntry().getType())) { + long numFrames = getTrackMetaData().getTimescale() * ms / 1000 / 1024; + long standZeit = getTrackMetaData().getTimescale() * ms / numFrames / 1000; + entry = new TimeToSampleBox.Entry(numFrames, standZeit); + + + while (numFrames-- > 0) { + samples.add((ByteBuffer) ByteBuffer.wrap(new byte[]{ + 0x21, 0x10, 0x04, 0x60, (byte) 0x8c, 0x1c, + }).rewind()); + } + + } else { + throw new RuntimeException("Tracks of type " + ofType.getClass().getSimpleName() + " are not supported"); + } + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return source.getSampleDescriptionBox(); + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + return Collections.singletonList(entry); + + } + + public TrackMetaData getTrackMetaData() { + return source.getTrackMetaData(); + } + + public String getHandler() { + return source.getHandler(); + } + + public boolean isEnabled() { + return source.isEnabled(); + } + + public boolean isInMovie() { + return source.isInMovie(); + } + + public boolean isInPreview() { + return source.isInPreview(); + } + + public boolean isInPoster() { + return source.isInPoster(); + } + + public List<ByteBuffer> getSamples() { + return samples; + } + + public Box getMediaHeaderBox() { + return source.getMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + +} diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/TextTrackImpl.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/TextTrackImpl.java new file mode 100644 index 0000000..3bae143 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/tracks/TextTrackImpl.java @@ -0,0 +1,165 @@ +/* + * 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.tracks; + +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.sampleentry.TextSampleEntry; +import com.googlecode.mp4parser.authoring.AbstractTrack; +import com.googlecode.mp4parser.authoring.TrackMetaData; +import com.googlecode.mp4parser.boxes.threegpp26245.FontTableBox; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +public class TextTrackImpl extends AbstractTrack { + TrackMetaData trackMetaData = new TrackMetaData(); + SampleDescriptionBox sampleDescriptionBox; + List<Line> subs = new LinkedList<Line>(); + + public List<Line> getSubs() { + return subs; + } + + public TextTrackImpl() { + sampleDescriptionBox = new SampleDescriptionBox(); + TextSampleEntry tx3g = new TextSampleEntry("tx3g"); + tx3g.setDataReferenceIndex(1); + tx3g.setStyleRecord(new TextSampleEntry.StyleRecord()); + tx3g.setBoxRecord(new TextSampleEntry.BoxRecord()); + sampleDescriptionBox.addBox(tx3g); + + FontTableBox ftab = new FontTableBox(); + ftab.setEntries(Collections.singletonList(new FontTableBox.FontRecord(1, "Serif"))); + + tx3g.addBox(ftab); + + + trackMetaData.setCreationTime(new Date()); + trackMetaData.setModificationTime(new Date()); + trackMetaData.setTimescale(1000); // Text tracks use millieseconds + + + } + + + public List<ByteBuffer> getSamples() { + List<ByteBuffer> samples = new LinkedList<ByteBuffer>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + samples.add(ByteBuffer.wrap(new byte[]{0, 0})); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + dos.writeShort(sub.text.getBytes("UTF-8").length); + dos.write(sub.text.getBytes("UTF-8")); + dos.close(); + } catch (IOException e) { + throw new Error("VM is broken. Does not support UTF-8"); + } + samples.add(ByteBuffer.wrap(baos.toByteArray())); + lastEnd = sub.to; + } + return samples; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public List<TimeToSampleBox.Entry> getDecodingTimeEntries() { + List<TimeToSampleBox.Entry> stts = new LinkedList<TimeToSampleBox.Entry>(); + long lastEnd = 0; + for (Line sub : subs) { + long silentTime = sub.from - lastEnd; + if (silentTime > 0) { + stts.add(new TimeToSampleBox.Entry(1, silentTime)); + } else if (silentTime < 0) { + throw new Error("Subtitle display times may not intersect"); + } + stts.add(new TimeToSampleBox.Entry(1, sub.to - sub.from)); + lastEnd = sub.to; + } + return stts; + } + + public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() { + return null; + } + + public long[] getSyncSamples() { + return null; + } + + public List<SampleDependencyTypeBox.Entry> getSampleDependencies() { + return null; + } + + public TrackMetaData getTrackMetaData() { + return trackMetaData; + } + + public String getHandler() { + return "sbtl"; + } + + + public static class Line { + long from; + long to; + String text; + + + public Line(long from, long to, String text) { + this.from = from; + this.to = to; + this.text = text; + } + + public long getFrom() { + return from; + } + + public String getText() { + return text; + } + + public long getTo() { + return to; + } + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return new NullMediaHeaderBox(); + } + + public SubSampleInformationBox getSubsampleInformationBox() { + return null; + } +} |