SyncSampleIntersectFinderImpl.java revision dd9eb897ee7c7b507cbdcf80263bb4b5de6966bf
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 */ 16package com.googlecode.mp4parser.authoring.builder; 17 18import com.coremedia.iso.boxes.TimeToSampleBox; 19import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; 20import com.googlecode.mp4parser.authoring.Movie; 21import com.googlecode.mp4parser.authoring.Track; 22 23import java.util.*; 24import java.util.concurrent.ConcurrentHashMap; 25import java.util.logging.Logger; 26 27import 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 */ 34public 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