Home | History | Annotate | Download | only in builder
      1 /*
      2  * Copyright 2012 Sebastian Annies, Hamburg
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the License);
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an AS IS BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.googlecode.mp4parser.authoring.builder;
     17 
     18 import com.coremedia.iso.boxes.TimeToSampleBox;
     19 import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry;
     20 import com.googlecode.mp4parser.authoring.Movie;
     21 import com.googlecode.mp4parser.authoring.Track;
     22 
     23 import java.util.*;
     24 import java.util.concurrent.ConcurrentHashMap;
     25 import java.util.logging.Logger;
     26 
     27 import static com.googlecode.mp4parser.util.Math.lcm;
     28 
     29 /**
     30  * This <code>FragmentIntersectionFinder</code> cuts the input movie video tracks in
     31  * fragments of the same length exactly before the sync samples. Audio tracks are cut
     32  * into pieces of similar length.
     33  */
     34 public class SyncSampleIntersectFinderImpl implements FragmentIntersectionFinder {
     35 
     36     private static Logger LOG = Logger.getLogger(SyncSampleIntersectFinderImpl.class.getName());
     37     private static Map<CacheTuple, long[]> getTimesCache = new ConcurrentHashMap<CacheTuple, long[]>();
     38     private static Map<CacheTuple, long[]> getSampleNumbersCache = new ConcurrentHashMap<CacheTuple, long[]>();
     39 
     40     private final int minFragmentDurationSeconds;
     41 
     42     public SyncSampleIntersectFinderImpl() {
     43         minFragmentDurationSeconds = 0;
     44     }
     45 
     46     /**
     47      * Creates a <code>SyncSampleIntersectFinderImpl</code> that will not create any fragment
     48      * smaller than the given <code>minFragmentDurationSeconds</code>
     49      *
     50      * @param minFragmentDurationSeconds the smallest allowable duration of a fragment.
     51      */
     52     public SyncSampleIntersectFinderImpl(int minFragmentDurationSeconds) {
     53         this.minFragmentDurationSeconds = minFragmentDurationSeconds;
     54     }
     55 
     56     /**
     57      * Gets an array of sample numbers that are meant to be the first sample of each
     58      * chunk or fragment.
     59      *
     60      * @param track concerned track
     61      * @param movie the context of the track
     62      * @return an array containing the ordinal of each fragment's first sample
     63      */
     64     public long[] sampleNumbers(Track track, Movie movie) {
     65         final CacheTuple key = new CacheTuple(track, movie);
     66         final long[] result = getSampleNumbersCache.get(key);
     67         if (result != null) {
     68             return result;
     69         }
     70 
     71         if ("vide".equals(track.getHandler())) {
     72             if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
     73                 List<long[]> times = getSyncSamplesTimestamps(movie, track);
     74                 final long[] commonIndices = getCommonIndices(track.getSyncSamples(), getTimes(track, movie), track.getTrackMetaData().getTimescale(), times.toArray(new long[times.size()][]));
     75                 getSampleNumbersCache.put(key, commonIndices);
     76                 return commonIndices;
     77             } else {
     78                 throw new RuntimeException("Video Tracks need sync samples. Only tracks other than video may have no sync samples.");
     79             }
     80         } else if ("soun".equals(track.getHandler())) {
     81             Track referenceTrack = null;
     82             for (Track candidate : movie.getTracks()) {
     83                 if (candidate.getSyncSamples() != null && "vide".equals(candidate.getHandler()) && candidate.getSyncSamples().length > 0) {
     84                     referenceTrack = candidate;
     85                 }
     86             }
     87             if (referenceTrack != null) {
     88 
     89                 // Gets the reference track's fra
     90                 long[] refSyncSamples = sampleNumbers(referenceTrack, movie);
     91 
     92                 int refSampleCount = referenceTrack.getSamples().size();
     93 
     94                 long[] syncSamples = new long[refSyncSamples.length];
     95                 long minSampleRate = 192000;
     96                 for (Track testTrack : movie.getTracks()) {
     97                     if ("soun".equals(testTrack.getHandler())) {
     98                         AudioSampleEntry ase = (AudioSampleEntry) testTrack.getSampleDescriptionBox().getSampleEntry();
     99                         if (ase.getSampleRate() < minSampleRate) {
    100                             minSampleRate = ase.getSampleRate();
    101                             long sc = testTrack.getSamples().size();
    102                             double stretch = (double) sc / refSampleCount;
    103                             TimeToSampleBox.Entry sttsEntry = testTrack.getDecodingTimeEntries().get(0);
    104                             long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types
    105 
    106                             for (int i = 0; i < syncSamples.length; i++) {
    107                                 long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1) * samplesPerFrame);
    108                                 syncSamples[i] = start;
    109                                 // The Stretch makes sure that there are as much audio and video chunks!
    110                             }
    111                             break;
    112                         }
    113                     }
    114                 }
    115                 AudioSampleEntry ase = (AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry();
    116                 TimeToSampleBox.Entry sttsEntry = track.getDecodingTimeEntries().get(0);
    117                 long samplesPerFrame = sttsEntry.getDelta(); // Assuming all audio tracks have the same number of samples per frame, which they do for all known types
    118                 double factor = (double) ase.getSampleRate() / (double) minSampleRate;
    119                 if (factor != Math.rint(factor)) { // Not an integer
    120                     throw new RuntimeException("Sample rates must be a multiple of the lowest sample rate to create a correct file!");
    121                 }
    122                 for (int i = 0; i < syncSamples.length; i++) {
    123                     syncSamples[i] = (long) (1 + syncSamples[i] * factor / (double) samplesPerFrame);
    124                 }
    125                 getSampleNumbersCache.put(key, syncSamples);
    126                 return syncSamples;
    127             }
    128             throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!");
    129         } else {
    130             // Ok, my track has no sync samples - let's find one with sync samples.
    131             for (Track candidate : movie.getTracks()) {
    132                 if (candidate.getSyncSamples() != null && candidate.getSyncSamples().length > 0) {
    133                     long[] refSyncSamples = sampleNumbers(candidate, movie);
    134                     int refSampleCount = candidate.getSamples().size();
    135 
    136                     long[] syncSamples = new long[refSyncSamples.length];
    137                     long sc = track.getSamples().size();
    138                     double stretch = (double) sc / refSampleCount;
    139 
    140                     for (int i = 0; i < syncSamples.length; i++) {
    141                         long start = (long) Math.ceil(stretch * (refSyncSamples[i] - 1)) + 1;
    142                         syncSamples[i] = start;
    143                         // The Stretch makes sure that there are as much audio and video chunks!
    144                     }
    145                     getSampleNumbersCache.put(key, syncSamples);
    146                     return syncSamples;
    147                 }
    148             }
    149             throw new RuntimeException("There was absolutely no Track with sync samples. I can't work with that!");
    150         }
    151 
    152 
    153     }
    154 
    155     /**
    156      * Calculates the timestamp of all tracks' sync samples.
    157      *
    158      * @param movie
    159      * @param track
    160      * @return
    161      */
    162     public static List<long[]> getSyncSamplesTimestamps(Movie movie, Track track) {
    163         List<long[]> times = new LinkedList<long[]>();
    164         for (Track currentTrack : movie.getTracks()) {
    165             if (currentTrack.getHandler().equals(track.getHandler())) {
    166                 long[] currentTrackSyncSamples = currentTrack.getSyncSamples();
    167                 if (currentTrackSyncSamples != null && currentTrackSyncSamples.length > 0) {
    168                     final long[] currentTrackTimes = getTimes(currentTrack, movie);
    169                     times.add(currentTrackTimes);
    170                 }
    171             }
    172         }
    173         return times;
    174     }
    175 
    176     public long[] getCommonIndices(long[] syncSamples, long[] syncSampleTimes, long timeScale, long[]... otherTracksTimes) {
    177         List<Long> nuSyncSamples = new LinkedList<Long>();
    178         List<Long> nuSyncSampleTimes = new LinkedList<Long>();
    179 
    180 
    181         for (int i = 0; i < syncSampleTimes.length; i++) {
    182             boolean foundInEveryRef = true;
    183             for (long[] times : otherTracksTimes) {
    184                 foundInEveryRef &= (Arrays.binarySearch(times, syncSampleTimes[i]) >= 0);
    185             }
    186 
    187             if (foundInEveryRef) {
    188                 // use sample only if found in every other track.
    189                 nuSyncSamples.add(syncSamples[i]);
    190                 nuSyncSampleTimes.add(syncSampleTimes[i]);
    191             }
    192         }
    193         // We have two arrays now:
    194         // nuSyncSamples: Contains all common sync samples
    195         // nuSyncSampleTimes: Contains the times of all sync samples
    196 
    197         // Start: Warn user if samples are not matching!
    198         if (nuSyncSamples.size() < (syncSamples.length * 0.25)) {
    199             String log = "";
    200             log += String.format("%5d - Common:  [", nuSyncSamples.size());
    201             for (long l : nuSyncSamples) {
    202                 log += (String.format("%10d,", l));
    203             }
    204             log += ("]");
    205             LOG.warning(log);
    206             log = "";
    207 
    208             log += String.format("%5d - In    :  [", syncSamples.length);
    209             for (long l : syncSamples) {
    210                 log += (String.format("%10d,", l));
    211             }
    212             log += ("]");
    213             LOG.warning(log);
    214             LOG.warning("There are less than 25% of common sync samples in the given track.");
    215             throw new RuntimeException("There are less than 25% of common sync samples in the given track.");
    216         } else if (nuSyncSamples.size() < (syncSamples.length * 0.5)) {
    217             LOG.fine("There are less than 50% of common sync samples in the given track. This is implausible but I'm ok to continue.");
    218         } else if (nuSyncSamples.size() < syncSamples.length) {
    219             LOG.finest("Common SyncSample positions vs. this tracks SyncSample positions: " + nuSyncSamples.size() + " vs. " + syncSamples.length);
    220         }
    221         // End: Warn user if samples are not matching!
    222 
    223 
    224 
    225 
    226         List<Long> finalSampleList = new LinkedList<Long>();
    227 
    228         if (minFragmentDurationSeconds > 0) {
    229             // if minFragmentDurationSeconds is greater 0
    230             // we need to throw away certain samples.
    231             long lastSyncSampleTime = -1;
    232             Iterator<Long> nuSyncSamplesIterator = nuSyncSamples.iterator();
    233             Iterator<Long> nuSyncSampleTimesIterator = nuSyncSampleTimes.iterator();
    234             while (nuSyncSamplesIterator.hasNext() && nuSyncSampleTimesIterator.hasNext()) {
    235                 long curSyncSample = nuSyncSamplesIterator.next();
    236                 long curSyncSampleTime = nuSyncSampleTimesIterator.next();
    237                 if (lastSyncSampleTime == -1 || (curSyncSampleTime - lastSyncSampleTime) / timeScale >= minFragmentDurationSeconds) {
    238                     finalSampleList.add(curSyncSample);
    239                     lastSyncSampleTime = curSyncSampleTime;
    240                 }
    241             }
    242         } else {
    243             // the list of all samples is the final list of samples
    244             // since minFragmentDurationSeconds ist not used.
    245             finalSampleList = nuSyncSamples;
    246         }
    247 
    248 
    249         // transform the list to an array
    250         long[] finalSampleArray = new long[finalSampleList.size()];
    251         for (int i = 0; i < finalSampleArray.length; i++) {
    252             finalSampleArray[i] = finalSampleList.get(i);
    253         }
    254         return finalSampleArray;
    255 
    256     }
    257 
    258 
    259     private static long[] getTimes(Track track, Movie m) {
    260         final CacheTuple key = new CacheTuple(track, m);
    261         final long[] result = getTimesCache.get(key);
    262         if (result != null) {
    263             return result;
    264         }
    265 
    266         long[] syncSamples = track.getSyncSamples();
    267         long[] syncSampleTimes = new long[syncSamples.length];
    268         Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries());
    269 
    270         int currentSample = 1;  // first syncsample is 1
    271         long currentDuration = 0;
    272         long currentDelta = 0;
    273         int currentSyncSampleIndex = 0;
    274         long left = 0;
    275 
    276         final long scalingFactor = calculateTracktimesScalingFactor(m, track);
    277 
    278         while (currentSample <= syncSamples[syncSamples.length - 1]) {
    279             if (currentSample++ == syncSamples[currentSyncSampleIndex]) {
    280                 syncSampleTimes[currentSyncSampleIndex++] = currentDuration * scalingFactor;
    281             }
    282             if (left-- == 0) {
    283                 TimeToSampleBox.Entry entry = timeQueue.poll();
    284                 left = entry.getCount() - 1;
    285                 currentDelta = entry.getDelta();
    286             }
    287             currentDuration += currentDelta;
    288         }
    289         getTimesCache.put(key, syncSampleTimes);
    290         return syncSampleTimes;
    291     }
    292 
    293     private static long calculateTracktimesScalingFactor(Movie m, Track track) {
    294         long timeScale = 1;
    295         for (Track track1 : m.getTracks()) {
    296             if (track1.getHandler().equals(track.getHandler())) {
    297                 if (track1.getTrackMetaData().getTimescale() != track.getTrackMetaData().getTimescale()) {
    298                     timeScale = lcm(timeScale, track1.getTrackMetaData().getTimescale());
    299                 }
    300             }
    301         }
    302         return timeScale;
    303     }
    304 
    305     public static class CacheTuple {
    306         Track track;
    307         Movie movie;
    308 
    309         public CacheTuple(Track track, Movie movie) {
    310             this.track = track;
    311             this.movie = movie;
    312         }
    313 
    314         @Override
    315         public boolean equals(Object o) {
    316             if (this == o) return true;
    317             if (o == null || getClass() != o.getClass()) return false;
    318 
    319             CacheTuple that = (CacheTuple) o;
    320 
    321             if (movie != null ? !movie.equals(that.movie) : that.movie != null) return false;
    322             if (track != null ? !track.equals(that.track) : that.track != null) return false;
    323 
    324             return true;
    325         }
    326 
    327         @Override
    328         public int hashCode() {
    329             int result = track != null ? track.hashCode() : 0;
    330             result = 31 * result + (movie != null ? movie.hashCode() : 0);
    331             return result;
    332         }
    333     }
    334 }
    335