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