DefaultMp4Builder.java revision dd9eb897ee7c7b507cbdcf80263bb4b5de6966bf
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.BoxParser; 19import com.coremedia.iso.IsoFile; 20import com.coremedia.iso.IsoTypeWriter; 21import com.coremedia.iso.boxes.Box; 22import com.coremedia.iso.boxes.CompositionTimeToSample; 23import com.coremedia.iso.boxes.ContainerBox; 24import com.coremedia.iso.boxes.DataEntryUrlBox; 25import com.coremedia.iso.boxes.DataInformationBox; 26import com.coremedia.iso.boxes.DataReferenceBox; 27import com.coremedia.iso.boxes.FileTypeBox; 28import com.coremedia.iso.boxes.HandlerBox; 29import com.coremedia.iso.boxes.MediaBox; 30import com.coremedia.iso.boxes.MediaHeaderBox; 31import com.coremedia.iso.boxes.MediaInformationBox; 32import com.coremedia.iso.boxes.MovieBox; 33import com.coremedia.iso.boxes.MovieHeaderBox; 34import com.coremedia.iso.boxes.SampleDependencyTypeBox; 35import com.coremedia.iso.boxes.SampleSizeBox; 36import com.coremedia.iso.boxes.SampleTableBox; 37import com.coremedia.iso.boxes.SampleToChunkBox; 38import com.coremedia.iso.boxes.StaticChunkOffsetBox; 39import com.coremedia.iso.boxes.SyncSampleBox; 40import com.coremedia.iso.boxes.TimeToSampleBox; 41import com.coremedia.iso.boxes.TrackBox; 42import com.coremedia.iso.boxes.TrackHeaderBox; 43import com.googlecode.mp4parser.authoring.DateHelper; 44import com.googlecode.mp4parser.authoring.Movie; 45import com.googlecode.mp4parser.authoring.Track; 46 47import java.io.IOException; 48import java.nio.ByteBuffer; 49import java.nio.MappedByteBuffer; 50import java.nio.channels.GatheringByteChannel; 51import java.nio.channels.ReadableByteChannel; 52import java.nio.channels.WritableByteChannel; 53import java.util.ArrayList; 54import java.util.Date; 55import java.util.HashMap; 56import java.util.HashSet; 57import java.util.LinkedList; 58import java.util.List; 59import java.util.Map; 60import java.util.Set; 61import java.util.logging.Level; 62import java.util.logging.Logger; 63 64import static com.googlecode.mp4parser.util.CastUtils.l2i; 65 66/** 67 * Creates a plain MP4 file from a video. Plain as plain can be. 68 */ 69public class DefaultMp4Builder implements Mp4Builder { 70 71 public int STEPSIZE = 64; 72 Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>(); 73 private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName()); 74 75 HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>(); 76 HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>(); 77 private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder(); 78 79 public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { 80 this.intersectionFinder = intersectionFinder; 81 } 82 83 /** 84 * {@inheritDoc} 85 */ 86 public IsoFile build(Movie movie) { 87 LOG.fine("Creating movie " + movie); 88 for (Track track : movie.getTracks()) { 89 // getting the samples may be a time consuming activity 90 List<ByteBuffer> samples = track.getSamples(); 91 putSamples(track, samples); 92 long[] sizes = new long[samples.size()]; 93 for (int i = 0; i < sizes.length; i++) { 94 sizes[i] = samples.get(i).limit(); 95 } 96 putSampleSizes(track, sizes); 97 } 98 99 IsoFile isoFile = new IsoFile(); 100 // ouch that is ugly but I don't know how to do it else 101 List<String> minorBrands = new LinkedList<String>(); 102 minorBrands.add("isom"); 103 minorBrands.add("iso2"); 104 minorBrands.add("avc1"); 105 106 isoFile.addBox(new FileTypeBox("isom", 0, minorBrands)); 107 isoFile.addBox(createMovieBox(movie)); 108 InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie); 109 isoFile.addBox(mdat); 110 111 /* 112 dataOffset is where the first sample starts. In this special mdat the samples always start 113 at offset 16 so that we can use the same offset for large boxes and small boxes 114 */ 115 long dataOffset = mdat.getDataOffset(); 116 for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) { 117 long[] offsets = chunkOffsetBox.getChunkOffsets(); 118 for (int i = 0; i < offsets.length; i++) { 119 offsets[i] += dataOffset; 120 } 121 } 122 123 124 return isoFile; 125 } 126 127 public FragmentIntersectionFinder getFragmentIntersectionFinder() { 128 throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!"); 129 } 130 131 protected long[] putSampleSizes(Track track, long[] sizes) { 132 return track2SampleSizes.put(track, sizes); 133 } 134 135 protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) { 136 return track2Sample.put(track, samples); 137 } 138 139 private MovieBox createMovieBox(Movie movie) { 140 MovieBox movieBox = new MovieBox(); 141 MovieHeaderBox mvhd = new MovieHeaderBox(); 142 143 mvhd.setCreationTime(DateHelper.convert(new Date())); 144 mvhd.setModificationTime(DateHelper.convert(new Date())); 145 146 long movieTimeScale = getTimescale(movie); 147 long duration = 0; 148 149 for (Track track : movie.getTracks()) { 150 long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); 151 if (tracksDuration > duration) { 152 duration = tracksDuration; 153 } 154 155 156 } 157 158 mvhd.setDuration(duration); 159 mvhd.setTimescale(movieTimeScale); 160 // find the next available trackId 161 long nextTrackId = 0; 162 for (Track track : movie.getTracks()) { 163 nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; 164 } 165 mvhd.setNextTrackId(++nextTrackId); 166 if (mvhd.getCreationTime() >= 1l << 32 || 167 mvhd.getModificationTime() >= 1l << 32 || 168 mvhd.getDuration() >= 1l << 32) { 169 mvhd.setVersion(1); 170 } 171 172 movieBox.addBox(mvhd); 173 for (Track track : movie.getTracks()) { 174 movieBox.addBox(createTrackBox(track, movie)); 175 } 176 // metadata here 177 Box udta = createUdta(movie); 178 if (udta != null) { 179 movieBox.addBox(udta); 180 } 181 return movieBox; 182 183 } 184 185 /** 186 * Override to create a user data box that may contain metadata. 187 * 188 * @return a 'udta' box or <code>null</code> if none provided 189 */ 190 protected Box createUdta(Movie movie) { 191 return null; 192 } 193 194 private TrackBox createTrackBox(Track track, Movie movie) { 195 196 LOG.info("Creating Mp4TrackImpl " + track); 197 TrackBox trackBox = new TrackBox(); 198 TrackHeaderBox tkhd = new TrackHeaderBox(); 199 int flags = 0; 200 if (track.isEnabled()) { 201 flags += 1; 202 } 203 204 if (track.isInMovie()) { 205 flags += 2; 206 } 207 208 if (track.isInPreview()) { 209 flags += 4; 210 } 211 212 if (track.isInPoster()) { 213 flags += 8; 214 } 215 tkhd.setFlags(flags); 216 217 tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); 218 tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); 219 // We need to take edit list box into account in trackheader duration 220 // but as long as I don't support edit list boxes it is sufficient to 221 // just translate media duration to movie timescale 222 tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale()); 223 tkhd.setHeight(track.getTrackMetaData().getHeight()); 224 tkhd.setWidth(track.getTrackMetaData().getWidth()); 225 tkhd.setLayer(track.getTrackMetaData().getLayer()); 226 tkhd.setModificationTime(DateHelper.convert(new Date())); 227 tkhd.setTrackId(track.getTrackMetaData().getTrackId()); 228 tkhd.setVolume(track.getTrackMetaData().getVolume()); 229 if (tkhd.getCreationTime() >= 1l << 32 || 230 tkhd.getModificationTime() >= 1l << 32 || 231 tkhd.getDuration() >= 1l << 32) { 232 tkhd.setVersion(1); 233 } 234 235 trackBox.addBox(tkhd); 236 237/* 238 EditBox edit = new EditBox(); 239 EditListBox editListBox = new EditListBox(); 240 editListBox.setEntries(Collections.singletonList( 241 new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1))); 242 edit.addBox(editListBox); 243 trackBox.addBox(edit); 244*/ 245 246 MediaBox mdia = new MediaBox(); 247 trackBox.addBox(mdia); 248 MediaHeaderBox mdhd = new MediaHeaderBox(); 249 mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); 250 mdhd.setDuration(getDuration(track)); 251 mdhd.setTimescale(track.getTrackMetaData().getTimescale()); 252 mdhd.setLanguage(track.getTrackMetaData().getLanguage()); 253 mdia.addBox(mdhd); 254 HandlerBox hdlr = new HandlerBox(); 255 mdia.addBox(hdlr); 256 257 hdlr.setHandlerType(track.getHandler()); 258 259 MediaInformationBox minf = new MediaInformationBox(); 260 minf.addBox(track.getMediaHeaderBox()); 261 262 // dinf: all these three boxes tell us is that the actual 263 // data is in the current file and not somewhere external 264 DataInformationBox dinf = new DataInformationBox(); 265 DataReferenceBox dref = new DataReferenceBox(); 266 dinf.addBox(dref); 267 DataEntryUrlBox url = new DataEntryUrlBox(); 268 url.setFlags(1); 269 dref.addBox(url); 270 minf.addBox(dinf); 271 // 272 273 SampleTableBox stbl = new SampleTableBox(); 274 275 stbl.addBox(track.getSampleDescriptionBox()); 276 277 List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries(); 278 if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) { 279 TimeToSampleBox stts = new TimeToSampleBox(); 280 stts.setEntries(track.getDecodingTimeEntries()); 281 stbl.addBox(stts); 282 } 283 284 List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries(); 285 if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) { 286 CompositionTimeToSample ctts = new CompositionTimeToSample(); 287 ctts.setEntries(compositionTimeToSampleEntries); 288 stbl.addBox(ctts); 289 } 290 291 long[] syncSamples = track.getSyncSamples(); 292 if (syncSamples != null && syncSamples.length > 0) { 293 SyncSampleBox stss = new SyncSampleBox(); 294 stss.setSampleNumber(syncSamples); 295 stbl.addBox(stss); 296 } 297 298 if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { 299 SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox(); 300 sdtp.setEntries(track.getSampleDependencies()); 301 stbl.addBox(sdtp); 302 } 303 HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>(); 304 for (Track current : movie.getTracks()) { 305 track2ChunkSizes.put(current, getChunkSizes(current, movie)); 306 } 307 int[] tracksChunkSizes = track2ChunkSizes.get(track); 308 309 SampleToChunkBox stsc = new SampleToChunkBox(); 310 stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>()); 311 long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size 312 for (int i = 0; i < tracksChunkSizes.length; i++) { 313 // The sample description index references the sample description box 314 // that describes the samples of this chunk. My Tracks cannot have more 315 // than one sample description box. Therefore 1 is always right 316 // the first chunk has the number '1' 317 if (lastChunkSize != tracksChunkSizes[i]) { 318 stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1)); 319 lastChunkSize = tracksChunkSizes[i]; 320 } 321 } 322 stbl.addBox(stsc); 323 324 SampleSizeBox stsz = new SampleSizeBox(); 325 stsz.setSampleSizes(track2SampleSizes.get(track)); 326 327 stbl.addBox(stsz); 328 // The ChunkOffsetBox we create here is just a stub 329 // since we haven't created the whole structure we can't tell where the 330 // first chunk starts (mdat box). So I just let the chunk offset 331 // start at zero and I will add the mdat offset later. 332 StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); 333 this.chunkOffsetBoxes.add(stco); 334 long offset = 0; 335 long[] chunkOffset = new long[tracksChunkSizes.length]; 336 // all tracks have the same number of chunks 337 if (LOG.isLoggable(Level.FINE)) { 338 LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId()); 339 } 340 341 342 for (int i = 0; i < tracksChunkSizes.length; i++) { 343 // The filelayout will be: 344 // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n 345 // calculating the offsets 346 if (LOG.isLoggable(Level.FINER)) { 347 LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i); 348 } 349 for (Track current : movie.getTracks()) { 350 if (LOG.isLoggable(Level.FINEST)) { 351 LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId()); 352 } 353 int[] chunkSizes = track2ChunkSizes.get(current); 354 long firstSampleOfChunk = 0; 355 for (int j = 0; j < i; j++) { 356 firstSampleOfChunk += chunkSizes[j]; 357 } 358 if (current == track) { 359 chunkOffset[i] = offset; 360 } 361 for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { 362 offset += track2SampleSizes.get(current)[j]; 363 } 364 } 365 } 366 stco.setChunkOffsets(chunkOffset); 367 stbl.addBox(stco); 368 minf.addBox(stbl); 369 mdia.addBox(minf); 370 371 return trackBox; 372 } 373 374 private class InterleaveChunkMdat implements Box { 375 List<Track> tracks; 376 List<ByteBuffer> samples = new ArrayList<ByteBuffer>(); 377 ContainerBox parent; 378 379 long contentSize = 0; 380 381 public ContainerBox getParent() { 382 return parent; 383 } 384 385 public void setParent(ContainerBox parent) { 386 this.parent = parent; 387 } 388 389 public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { 390 } 391 392 private InterleaveChunkMdat(Movie movie) { 393 394 tracks = movie.getTracks(); 395 Map<Track, int[]> chunks = new HashMap<Track, int[]>(); 396 for (Track track : movie.getTracks()) { 397 chunks.put(track, getChunkSizes(track, movie)); 398 } 399 400 for (int i = 0; i < chunks.values().iterator().next().length; i++) { 401 for (Track track : tracks) { 402 403 int[] chunkSizes = chunks.get(track); 404 long firstSampleOfChunk = 0; 405 for (int j = 0; j < i; j++) { 406 firstSampleOfChunk += chunkSizes[j]; 407 } 408 409 for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { 410 411 ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j); 412 contentSize += s.limit(); 413 samples.add((ByteBuffer) s.rewind()); 414 } 415 416 } 417 418 } 419 420 } 421 422 public long getDataOffset() { 423 Box b = this; 424 long offset = 16; 425 while (b.getParent() != null) { 426 for (Box box : b.getParent().getBoxes()) { 427 if (b == box) { 428 break; 429 } 430 offset += box.getSize(); 431 } 432 b = b.getParent(); 433 } 434 return offset; 435 } 436 437 438 public String getType() { 439 return "mdat"; 440 } 441 442 public long getSize() { 443 return 16 + contentSize; 444 } 445 446 private boolean isSmallBox(long contentSize) { 447 return (contentSize + 8) < 4294967296L; 448 } 449 450 451 public void getBox(WritableByteChannel writableByteChannel) throws IOException { 452 ByteBuffer bb = ByteBuffer.allocate(16); 453 long size = getSize(); 454 if (isSmallBox(size)) { 455 IsoTypeWriter.writeUInt32(bb, size); 456 } else { 457 IsoTypeWriter.writeUInt32(bb, 1); 458 } 459 bb.put(IsoFile.fourCCtoBytes("mdat")); 460 if (isSmallBox(size)) { 461 bb.put(new byte[8]); 462 } else { 463 IsoTypeWriter.writeUInt64(bb, size); 464 } 465 bb.rewind(); 466 writableByteChannel.write(bb); 467 if (writableByteChannel instanceof GatheringByteChannel) { 468 List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples); 469 470 471 for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) { 472 List<ByteBuffer> sublist = nuSamples.subList( 473 i * STEPSIZE, // start 474 (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end 475 ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); 476 do { 477 ((GatheringByteChannel) writableByteChannel).write(sampleArray); 478 } while (sampleArray[sampleArray.length - 1].remaining() > 0); 479 } 480 //System.err.println(bytesWritten); 481 } else { 482 for (ByteBuffer sample : samples) { 483 sample.rewind(); 484 writableByteChannel.write(sample); 485 } 486 } 487 } 488 489 } 490 491 /** 492 * Gets the chunk sizes for the given track. 493 * 494 * @param track 495 * @param movie 496 * @return 497 */ 498 int[] getChunkSizes(Track track, Movie movie) { 499 500 long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie); 501 int[] chunkSizes = new int[referenceChunkStarts.length]; 502 503 504 for (int i = 0; i < referenceChunkStarts.length; i++) { 505 long start = referenceChunkStarts[i] - 1; 506 long end; 507 if (referenceChunkStarts.length == i + 1) { 508 end = track.getSamples().size(); 509 } else { 510 end = referenceChunkStarts[i + 1] - 1; 511 } 512 513 chunkSizes[i] = l2i(end - start); 514 // The Stretch makes sure that there are as much audio and video chunks! 515 } 516 assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal"; 517 return chunkSizes; 518 519 520 } 521 522 523 private static long sum(int[] ls) { 524 long rc = 0; 525 for (long l : ls) { 526 rc += l; 527 } 528 return rc; 529 } 530 531 protected static long getDuration(Track track) { 532 long duration = 0; 533 for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { 534 duration += entry.getCount() * entry.getDelta(); 535 } 536 return duration; 537 } 538 539 public long getTimescale(Movie movie) { 540 long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale(); 541 for (Track track : movie.getTracks()) { 542 timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); 543 } 544 return timescale; 545 } 546 547 public static long gcd(long a, long b) { 548 if (b == 0) { 549 return a; 550 } 551 return gcd(b, a % b); 552 } 553 554 public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) { 555 ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); 556 for (ByteBuffer buffer : samples) { 557 int lastIndex = nuSamples.size() - 1; 558 if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && 559 nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { 560 ByteBuffer oldBuffer = nuSamples.remove(lastIndex); 561 ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); 562 // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. 563 nuSamples.add(nu); 564 } else if (lastIndex >= 0 && 565 buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && 566 nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { 567 // This can go wrong - but will it? 568 ByteBuffer oldBuffer = nuSamples.get(lastIndex); 569 oldBuffer.limit(buffer.limit() + oldBuffer.limit()); 570 } else { 571 nuSamples.add(buffer); 572 } 573 } 574 return nuSamples; 575 } 576} 577