/* * 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 FragmentIntersectionFinder 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 getTimesCache = new ConcurrentHashMap(); private static Map getSampleNumbersCache = new ConcurrentHashMap(); private final int minFragmentDurationSeconds; public SyncSampleIntersectFinderImpl() { minFragmentDurationSeconds = 0; } /** * Creates a SyncSampleIntersectFinderImpl that will not create any fragment * smaller than the given minFragmentDurationSeconds * * @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 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 getSyncSamplesTimestamps(Movie movie, Track track) { List times = new LinkedList(); 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 nuSyncSamples = new LinkedList(); List nuSyncSampleTimes = new LinkedList(); 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 finalSampleList = new LinkedList(); if (minFragmentDurationSeconds > 0) { // if minFragmentDurationSeconds is greater 0 // we need to throw away certain samples. long lastSyncSampleTime = -1; Iterator nuSyncSamplesIterator = nuSyncSamples.iterator(); Iterator 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 timeQueue = new LinkedList(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; } } }