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