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.tracks;
17
18import com.coremedia.iso.boxes.*;
19import com.googlecode.mp4parser.authoring.Track;
20import com.googlecode.mp4parser.authoring.TrackMetaData;
21
22import java.nio.ByteBuffer;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.Queue;
28import java.util.logging.Logger;
29
30/**
31 * Changes the timescale of a track by wrapping the track.
32 */
33public class ChangeTimeScaleTrack implements Track {
34    private static final Logger LOG = Logger.getLogger(ChangeTimeScaleTrack.class.getName());
35
36    Track source;
37    List<CompositionTimeToSample.Entry> ctts;
38    List<TimeToSampleBox.Entry> tts;
39    long timeScale;
40
41    /**
42     * Changes the time scale of the source track to the target time scale and makes sure
43     * that any rounding errors that may have summed are corrected exactly before the syncSamples.
44     *
45     * @param source          the source track
46     * @param targetTimeScale the resulting time scale of this track.
47     * @param syncSamples     at these sync points where rounding error are corrected.
48     */
49    public ChangeTimeScaleTrack(Track source, long targetTimeScale, long[] syncSamples) {
50        this.source = source;
51        this.timeScale = targetTimeScale;
52        double timeScaleFactor = (double) targetTimeScale / source.getTrackMetaData().getTimescale();
53        ctts = adjustCtts(source.getCompositionTimeEntries(), timeScaleFactor);
54        tts = adjustTts(source.getDecodingTimeEntries(), timeScaleFactor, syncSamples, getTimes(source, syncSamples, targetTimeScale));
55    }
56
57    private static long[] getTimes(Track track, long[] syncSamples, long targetTimeScale) {
58        long[] syncSampleTimes = new long[syncSamples.length];
59        Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries());
60
61        int currentSample = 1;  // first syncsample is 1
62        long currentDuration = 0;
63        long currentDelta = 0;
64        int currentSyncSampleIndex = 0;
65        long left = 0;
66
67
68        while (currentSample <= syncSamples[syncSamples.length - 1]) {
69            if (currentSample++ == syncSamples[currentSyncSampleIndex]) {
70                syncSampleTimes[currentSyncSampleIndex++] = (currentDuration * targetTimeScale) / track.getTrackMetaData().getTimescale();
71            }
72            if (left-- == 0) {
73                TimeToSampleBox.Entry entry = timeQueue.poll();
74                left = entry.getCount() - 1;
75                currentDelta = entry.getDelta();
76            }
77            currentDuration += currentDelta;
78        }
79        return syncSampleTimes;
80
81    }
82
83    public SampleDescriptionBox getSampleDescriptionBox() {
84        return source.getSampleDescriptionBox();
85    }
86
87    public List<TimeToSampleBox.Entry> getDecodingTimeEntries() {
88        return tts;
89    }
90
91    public List<CompositionTimeToSample.Entry> getCompositionTimeEntries() {
92        return ctts;
93    }
94
95    public long[] getSyncSamples() {
96        return source.getSyncSamples();
97    }
98
99    public List<SampleDependencyTypeBox.Entry> getSampleDependencies() {
100        return source.getSampleDependencies();
101    }
102
103    public TrackMetaData getTrackMetaData() {
104        TrackMetaData trackMetaData = (TrackMetaData) source.getTrackMetaData().clone();
105        trackMetaData.setTimescale(timeScale);
106        return trackMetaData;
107    }
108
109    public String getHandler() {
110        return source.getHandler();
111    }
112
113    public boolean isEnabled() {
114        return source.isEnabled();
115    }
116
117    public boolean isInMovie() {
118        return source.isInMovie();
119    }
120
121    public boolean isInPreview() {
122        return source.isInPreview();
123    }
124
125    public boolean isInPoster() {
126        return source.isInPoster();
127    }
128
129    public List<ByteBuffer> getSamples() {
130        return source.getSamples();
131    }
132
133
134    /**
135     * Adjusting the composition times is easy. Just scale it by the factor - that's it. There is no rounding
136     * error summing up.
137     *
138     * @param source
139     * @param timeScaleFactor
140     * @return
141     */
142    static List<CompositionTimeToSample.Entry> adjustCtts(List<CompositionTimeToSample.Entry> source, double timeScaleFactor) {
143        if (source != null) {
144            List<CompositionTimeToSample.Entry> entries2 = new ArrayList<CompositionTimeToSample.Entry>(source.size());
145            for (CompositionTimeToSample.Entry entry : source) {
146                entries2.add(new CompositionTimeToSample.Entry(entry.getCount(), (int) Math.round(timeScaleFactor * entry.getOffset())));
147            }
148            return entries2;
149        } else {
150            return null;
151        }
152    }
153
154    static List<TimeToSampleBox.Entry> adjustTts(List<TimeToSampleBox.Entry> source, double timeScaleFactor, long[] syncSample, long[] syncSampleTimes) {
155
156        long[] sourceArray = TimeToSampleBox.blowupTimeToSamples(source);
157        long summedDurations = 0;
158
159        LinkedList<TimeToSampleBox.Entry> entries2 = new LinkedList<TimeToSampleBox.Entry>();
160        for (int i = 1; i <= sourceArray.length; i++) {
161            long duration = sourceArray[i - 1];
162
163            long x = Math.round(timeScaleFactor * duration);
164
165
166            TimeToSampleBox.Entry last = entries2.peekLast();
167            int ssIndex;
168            if ((ssIndex = Arrays.binarySearch(syncSample, i + 1)) >= 0) {
169                // we are at the sample before sync point
170                if (syncSampleTimes[ssIndex] != summedDurations) {
171                    long correction = syncSampleTimes[ssIndex] - (summedDurations + x);
172                    LOG.finest(String.format("Sample %d %d / %d - correct by %d", i, summedDurations, syncSampleTimes[ssIndex], correction));
173                    x += correction;
174                }
175            }
176            summedDurations += x;
177            if (last == null) {
178                entries2.add(new TimeToSampleBox.Entry(1, x));
179            } else if (last.getDelta() != x) {
180                entries2.add(new TimeToSampleBox.Entry(1, x));
181            } else {
182                last.setCount(last.getCount() + 1);
183            }
184
185        }
186        return entries2;
187    }
188
189    public Box getMediaHeaderBox() {
190        return source.getMediaHeaderBox();
191    }
192
193    public SubSampleInformationBox getSubsampleInformationBox() {
194        return source.getSubsampleInformationBox();
195    }
196
197    @Override
198    public String toString() {
199        return "ChangeTimeScaleTrack{" +
200                "source=" + source +
201                '}';
202    }
203}