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.adaptivestreaming;
17
18import com.coremedia.iso.IsoFile;
19import com.coremedia.iso.boxes.Box;
20import com.coremedia.iso.boxes.SoundMediaHeaderBox;
21import com.coremedia.iso.boxes.VideoMediaHeaderBox;
22import com.coremedia.iso.boxes.fragment.MovieFragmentBox;
23import com.googlecode.mp4parser.authoring.Movie;
24import com.googlecode.mp4parser.authoring.Track;
25import com.googlecode.mp4parser.authoring.builder.*;
26import com.googlecode.mp4parser.authoring.tracks.ChangeTimeScaleTrack;
27
28import java.io.File;
29import java.io.FileOutputStream;
30import java.io.FileWriter;
31import java.io.IOException;
32import java.nio.channels.FileChannel;
33import java.util.Iterator;
34import java.util.logging.Logger;
35
36public class FlatPackageWriterImpl implements PackageWriter {
37    private static Logger LOG = Logger.getLogger(FlatPackageWriterImpl.class.getName());
38    long timeScale = 10000000;
39
40    private File outputDirectory;
41    private boolean debugOutput;
42    private FragmentedMp4Builder ismvBuilder;
43    ManifestWriter manifestWriter;
44
45    public FlatPackageWriterImpl() {
46        ismvBuilder = new FragmentedMp4Builder();
47        FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl();
48        ismvBuilder.setIntersectionFinder(intersectionFinder);
49        manifestWriter = new FlatManifestWriterImpl(intersectionFinder);
50    }
51
52    /**
53     * Creates a factory for a smooth streaming package. A smooth streaming package is
54     * a collection of files that can be served by a webserver as a smooth streaming
55     * stream.
56     * @param minFragmentDuration the smallest allowable duration of a fragment (0 == no restriction).
57     */
58    public FlatPackageWriterImpl(int minFragmentDuration) {
59        ismvBuilder = new FragmentedMp4Builder();
60        FragmentIntersectionFinder intersectionFinder = new SyncSampleIntersectFinderImpl(minFragmentDuration);
61        ismvBuilder.setIntersectionFinder(intersectionFinder);
62        manifestWriter = new FlatManifestWriterImpl(intersectionFinder);
63    }
64
65    public void setOutputDirectory(File outputDirectory) {
66        assert outputDirectory.isDirectory();
67        this.outputDirectory = outputDirectory;
68
69    }
70
71    public void setDebugOutput(boolean debugOutput) {
72        this.debugOutput = debugOutput;
73    }
74
75    public void setIsmvBuilder(FragmentedMp4Builder ismvBuilder) {
76        this.ismvBuilder = ismvBuilder;
77        this.manifestWriter = new FlatManifestWriterImpl(ismvBuilder.getFragmentIntersectionFinder());
78    }
79
80    public void setManifestWriter(ManifestWriter manifestWriter) {
81        this.manifestWriter = manifestWriter;
82    }
83
84    /**
85     * Writes the movie given as <code>qualities</code> flattened into the
86     * <code>outputDirectory</code>.
87     *
88     * @param source the source movie with all qualities
89     * @throws IOException
90     */
91    public void write(Movie source) throws IOException {
92
93        if (debugOutput) {
94            outputDirectory.mkdirs();
95            DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder();
96            IsoFile muxed = defaultMp4Builder.build(source);
97            File muxedFile = new File(outputDirectory, "debug_1_muxed.mp4");
98            FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile);
99            muxed.getBox(muxedFileOutputStream.getChannel());
100            muxedFileOutputStream.close();
101        }
102        Movie cleanedSource = removeUnknownTracks(source);
103        Movie movieWithAdjustedTimescale = correctTimescale(cleanedSource);
104
105        if (debugOutput) {
106            DefaultMp4Builder defaultMp4Builder = new DefaultMp4Builder();
107            IsoFile muxed = defaultMp4Builder.build(movieWithAdjustedTimescale);
108            File muxedFile = new File(outputDirectory, "debug_2_timescale.mp4");
109            FileOutputStream muxedFileOutputStream = new FileOutputStream(muxedFile);
110            muxed.getBox(muxedFileOutputStream.getChannel());
111            muxedFileOutputStream.close();
112        }
113        IsoFile isoFile = ismvBuilder.build(movieWithAdjustedTimescale);
114        if (debugOutput) {
115            File allQualities = new File(outputDirectory, "debug_3_fragmented.mp4");
116            FileOutputStream allQualis = new FileOutputStream(allQualities);
117            isoFile.getBox(allQualis.getChannel());
118            allQualis.close();
119        }
120
121
122        for (Track track : movieWithAdjustedTimescale.getTracks()) {
123            String bitrate = Long.toString(manifestWriter.getBitrate(track));
124            long trackId = track.getTrackMetaData().getTrackId();
125            Iterator<Box> boxIt = isoFile.getBoxes().iterator();
126            File mediaOutDir;
127            if (track.getMediaHeaderBox() instanceof SoundMediaHeaderBox) {
128                mediaOutDir = new File(outputDirectory, "audio");
129
130            } else if (track.getMediaHeaderBox() instanceof VideoMediaHeaderBox) {
131                mediaOutDir = new File(outputDirectory, "video");
132            } else {
133                System.err.println("Skipping Track with handler " + track.getHandler() + " and " + track.getMediaHeaderBox().getClass().getSimpleName());
134                continue;
135            }
136            File bitRateOutputDir = new File(mediaOutDir, bitrate);
137            bitRateOutputDir.mkdirs();
138            LOG.finer("Created : " + bitRateOutputDir.getCanonicalPath());
139
140            long[] fragmentTimes = manifestWriter.calculateFragmentDurations(track, movieWithAdjustedTimescale);
141            long startTime = 0;
142            int currentFragment = 0;
143            while (boxIt.hasNext()) {
144                Box b = boxIt.next();
145                if (b instanceof MovieFragmentBox) {
146                    assert ((MovieFragmentBox) b).getTrackCount() == 1;
147                    if (((MovieFragmentBox) b).getTrackNumbers()[0] == trackId) {
148                        FileOutputStream fos = new FileOutputStream(new File(bitRateOutputDir, Long.toString(startTime)));
149                        startTime += fragmentTimes[currentFragment++];
150                        FileChannel fc = fos.getChannel();
151                        Box mdat = boxIt.next();
152                        assert mdat.getType().equals("mdat");
153                        b.getBox(fc); // moof
154                        mdat.getBox(fc); // mdat
155                        fc.truncate(fc.position());
156                        fc.close();
157                    }
158                }
159
160            }
161        }
162        FileWriter fw = new FileWriter(new File(outputDirectory, "Manifest"));
163        fw.write(manifestWriter.getManifest(movieWithAdjustedTimescale));
164        fw.close();
165
166    }
167
168    private Movie removeUnknownTracks(Movie source) {
169        Movie nuMovie = new Movie();
170        for (Track track : source.getTracks()) {
171            if ("vide".equals(track.getHandler()) || "soun".equals(track.getHandler())) {
172                nuMovie.addTrack(track);
173            } else {
174                LOG.fine("Removed track " + track);
175            }
176        }
177        return nuMovie;
178    }
179
180
181    /**
182     * Returns a new <code>Movie</code> in that all tracks have the timescale 10000000. CTS & DTS are modified
183     * in a way that even with more than one framerate the fragments exactly begin at the same time.
184     *
185     * @param movie
186     * @return a movie with timescales suitable for smooth streaming manifests
187     */
188    public Movie correctTimescale(Movie movie) {
189        Movie nuMovie = new Movie();
190        for (Track track : movie.getTracks()) {
191            nuMovie.addTrack(new ChangeTimeScaleTrack(track, timeScale, ismvBuilder.getFragmentIntersectionFinder().sampleNumbers(track, movie)));
192        }
193        return nuMovie;
194
195    }
196
197}
198