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 */ 16 package com.googlecode.mp4parser.authoring.builder; 17 18 import com.coremedia.iso.BoxParser; 19 import com.coremedia.iso.IsoFile; 20 import com.coremedia.iso.IsoTypeWriter; 21 import com.coremedia.iso.boxes.Box; 22 import com.coremedia.iso.boxes.CompositionTimeToSample; 23 import com.coremedia.iso.boxes.ContainerBox; 24 import com.coremedia.iso.boxes.DataEntryUrlBox; 25 import com.coremedia.iso.boxes.DataInformationBox; 26 import com.coremedia.iso.boxes.DataReferenceBox; 27 import com.coremedia.iso.boxes.FileTypeBox; 28 import com.coremedia.iso.boxes.HandlerBox; 29 import com.coremedia.iso.boxes.MediaBox; 30 import com.coremedia.iso.boxes.MediaHeaderBox; 31 import com.coremedia.iso.boxes.MediaInformationBox; 32 import com.coremedia.iso.boxes.MovieBox; 33 import com.coremedia.iso.boxes.MovieHeaderBox; 34 import com.coremedia.iso.boxes.SampleDependencyTypeBox; 35 import com.coremedia.iso.boxes.SampleSizeBox; 36 import com.coremedia.iso.boxes.SampleTableBox; 37 import com.coremedia.iso.boxes.SampleToChunkBox; 38 import com.coremedia.iso.boxes.StaticChunkOffsetBox; 39 import com.coremedia.iso.boxes.SyncSampleBox; 40 import com.coremedia.iso.boxes.TimeToSampleBox; 41 import com.coremedia.iso.boxes.TrackBox; 42 import com.coremedia.iso.boxes.TrackHeaderBox; 43 import com.googlecode.mp4parser.authoring.DateHelper; 44 import com.googlecode.mp4parser.authoring.Movie; 45 import com.googlecode.mp4parser.authoring.Track; 46 47 import java.io.IOException; 48 import java.nio.ByteBuffer; 49 import java.nio.MappedByteBuffer; 50 import java.nio.channels.GatheringByteChannel; 51 import java.nio.channels.ReadableByteChannel; 52 import java.nio.channels.WritableByteChannel; 53 import java.util.ArrayList; 54 import java.util.Date; 55 import java.util.HashMap; 56 import java.util.HashSet; 57 import java.util.LinkedList; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 import java.util.logging.Level; 62 import java.util.logging.Logger; 63 64 import static com.googlecode.mp4parser.util.CastUtils.l2i; 65 66 /** 67 * Creates a plain MP4 file from a video. Plain as plain can be. 68 */ 69 public 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 tkhd.setMatrix(track.getTrackMetaData().getMatrix()); 230 if (tkhd.getCreationTime() >= 1l << 32 || 231 tkhd.getModificationTime() >= 1l << 32 || 232 tkhd.getDuration() >= 1l << 32) { 233 tkhd.setVersion(1); 234 } 235 236 trackBox.addBox(tkhd); 237 238 /* 239 EditBox edit = new EditBox(); 240 EditListBox editListBox = new EditListBox(); 241 editListBox.setEntries(Collections.singletonList( 242 new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1))); 243 edit.addBox(editListBox); 244 trackBox.addBox(edit); 245 */ 246 247 MediaBox mdia = new MediaBox(); 248 trackBox.addBox(mdia); 249 MediaHeaderBox mdhd = new MediaHeaderBox(); 250 mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); 251 mdhd.setDuration(getDuration(track)); 252 mdhd.setTimescale(track.getTrackMetaData().getTimescale()); 253 mdhd.setLanguage(track.getTrackMetaData().getLanguage()); 254 mdia.addBox(mdhd); 255 HandlerBox hdlr = new HandlerBox(); 256 mdia.addBox(hdlr); 257 258 hdlr.setHandlerType(track.getHandler()); 259 260 MediaInformationBox minf = new MediaInformationBox(); 261 minf.addBox(track.getMediaHeaderBox()); 262 263 // dinf: all these three boxes tell us is that the actual 264 // data is in the current file and not somewhere external 265 DataInformationBox dinf = new DataInformationBox(); 266 DataReferenceBox dref = new DataReferenceBox(); 267 dinf.addBox(dref); 268 DataEntryUrlBox url = new DataEntryUrlBox(); 269 url.setFlags(1); 270 dref.addBox(url); 271 minf.addBox(dinf); 272 // 273 274 SampleTableBox stbl = new SampleTableBox(); 275 276 stbl.addBox(track.getSampleDescriptionBox()); 277 278 List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries(); 279 if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) { 280 TimeToSampleBox stts = new TimeToSampleBox(); 281 stts.setEntries(track.getDecodingTimeEntries()); 282 stbl.addBox(stts); 283 } 284 285 List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries(); 286 if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) { 287 CompositionTimeToSample ctts = new CompositionTimeToSample(); 288 ctts.setEntries(compositionTimeToSampleEntries); 289 stbl.addBox(ctts); 290 } 291 292 long[] syncSamples = track.getSyncSamples(); 293 if (syncSamples != null && syncSamples.length > 0) { 294 SyncSampleBox stss = new SyncSampleBox(); 295 stss.setSampleNumber(syncSamples); 296 stbl.addBox(stss); 297 } 298 299 if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { 300 SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox(); 301 sdtp.setEntries(track.getSampleDependencies()); 302 stbl.addBox(sdtp); 303 } 304 HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>(); 305 for (Track current : movie.getTracks()) { 306 track2ChunkSizes.put(current, getChunkSizes(current, movie)); 307 } 308 int[] tracksChunkSizes = track2ChunkSizes.get(track); 309 310 SampleToChunkBox stsc = new SampleToChunkBox(); 311 stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>()); 312 long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size 313 for (int i = 0; i < tracksChunkSizes.length; i++) { 314 // The sample description index references the sample description box 315 // that describes the samples of this chunk. My Tracks cannot have more 316 // than one sample description box. Therefore 1 is always right 317 // the first chunk has the number '1' 318 if (lastChunkSize != tracksChunkSizes[i]) { 319 stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1)); 320 lastChunkSize = tracksChunkSizes[i]; 321 } 322 } 323 stbl.addBox(stsc); 324 325 SampleSizeBox stsz = new SampleSizeBox(); 326 stsz.setSampleSizes(track2SampleSizes.get(track)); 327 328 stbl.addBox(stsz); 329 // The ChunkOffsetBox we create here is just a stub 330 // since we haven't created the whole structure we can't tell where the 331 // first chunk starts (mdat box). So I just let the chunk offset 332 // start at zero and I will add the mdat offset later. 333 StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); 334 this.chunkOffsetBoxes.add(stco); 335 long offset = 0; 336 long[] chunkOffset = new long[tracksChunkSizes.length]; 337 // all tracks have the same number of chunks 338 if (LOG.isLoggable(Level.FINE)) { 339 LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId()); 340 } 341 342 343 for (int i = 0; i < tracksChunkSizes.length; i++) { 344 // The filelayout will be: 345 // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n 346 // calculating the offsets 347 if (LOG.isLoggable(Level.FINER)) { 348 LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i); 349 } 350 for (Track current : movie.getTracks()) { 351 if (LOG.isLoggable(Level.FINEST)) { 352 LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId()); 353 } 354 int[] chunkSizes = track2ChunkSizes.get(current); 355 long firstSampleOfChunk = 0; 356 for (int j = 0; j < i; j++) { 357 firstSampleOfChunk += chunkSizes[j]; 358 } 359 if (current == track) { 360 chunkOffset[i] = offset; 361 } 362 for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { 363 offset += track2SampleSizes.get(current)[j]; 364 } 365 } 366 } 367 stco.setChunkOffsets(chunkOffset); 368 stbl.addBox(stco); 369 minf.addBox(stbl); 370 mdia.addBox(minf); 371 372 return trackBox; 373 } 374 375 private class InterleaveChunkMdat implements Box { 376 List<Track> tracks; 377 List<ByteBuffer> samples = new ArrayList<ByteBuffer>(); 378 ContainerBox parent; 379 380 long contentSize = 0; 381 382 public ContainerBox getParent() { 383 return parent; 384 } 385 386 public void setParent(ContainerBox parent) { 387 this.parent = parent; 388 } 389 390 public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { 391 } 392 393 private InterleaveChunkMdat(Movie movie) { 394 395 tracks = movie.getTracks(); 396 Map<Track, int[]> chunks = new HashMap<Track, int[]>(); 397 for (Track track : movie.getTracks()) { 398 chunks.put(track, getChunkSizes(track, movie)); 399 } 400 401 for (int i = 0; i < chunks.values().iterator().next().length; i++) { 402 for (Track track : tracks) { 403 404 int[] chunkSizes = chunks.get(track); 405 long firstSampleOfChunk = 0; 406 for (int j = 0; j < i; j++) { 407 firstSampleOfChunk += chunkSizes[j]; 408 } 409 410 for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) { 411 412 ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j); 413 contentSize += s.limit(); 414 samples.add((ByteBuffer) s.rewind()); 415 } 416 417 } 418 419 } 420 421 } 422 423 public long getDataOffset() { 424 Box b = this; 425 long offset = 16; 426 while (b.getParent() != null) { 427 for (Box box : b.getParent().getBoxes()) { 428 if (b == box) { 429 break; 430 } 431 offset += box.getSize(); 432 } 433 b = b.getParent(); 434 } 435 return offset; 436 } 437 438 439 public String getType() { 440 return "mdat"; 441 } 442 443 public long getSize() { 444 return 16 + contentSize; 445 } 446 447 private boolean isSmallBox(long contentSize) { 448 return (contentSize + 8) < 4294967296L; 449 } 450 451 452 public void getBox(WritableByteChannel writableByteChannel) throws IOException { 453 ByteBuffer bb = ByteBuffer.allocate(16); 454 long size = getSize(); 455 if (isSmallBox(size)) { 456 IsoTypeWriter.writeUInt32(bb, size); 457 } else { 458 IsoTypeWriter.writeUInt32(bb, 1); 459 } 460 bb.put(IsoFile.fourCCtoBytes("mdat")); 461 if (isSmallBox(size)) { 462 bb.put(new byte[8]); 463 } else { 464 IsoTypeWriter.writeUInt64(bb, size); 465 } 466 bb.rewind(); 467 writableByteChannel.write(bb); 468 if (writableByteChannel instanceof GatheringByteChannel) { 469 List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples); 470 471 472 for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) { 473 List<ByteBuffer> sublist = nuSamples.subList( 474 i * STEPSIZE, // start 475 (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end 476 ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); 477 do { 478 ((GatheringByteChannel) writableByteChannel).write(sampleArray); 479 } while (sampleArray[sampleArray.length - 1].remaining() > 0); 480 } 481 //System.err.println(bytesWritten); 482 } else { 483 for (ByteBuffer sample : samples) { 484 sample.rewind(); 485 writableByteChannel.write(sample); 486 } 487 } 488 } 489 490 } 491 492 /** 493 * Gets the chunk sizes for the given track. 494 * 495 * @param track 496 * @param movie 497 * @return 498 */ 499 int[] getChunkSizes(Track track, Movie movie) { 500 501 long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie); 502 int[] chunkSizes = new int[referenceChunkStarts.length]; 503 504 505 for (int i = 0; i < referenceChunkStarts.length; i++) { 506 long start = referenceChunkStarts[i] - 1; 507 long end; 508 if (referenceChunkStarts.length == i + 1) { 509 end = track.getSamples().size(); 510 } else { 511 end = referenceChunkStarts[i + 1] - 1; 512 } 513 514 chunkSizes[i] = l2i(end - start); 515 // The Stretch makes sure that there are as much audio and video chunks! 516 } 517 assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal"; 518 return chunkSizes; 519 520 521 } 522 523 524 private static long sum(int[] ls) { 525 long rc = 0; 526 for (long l : ls) { 527 rc += l; 528 } 529 return rc; 530 } 531 532 protected static long getDuration(Track track) { 533 long duration = 0; 534 for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { 535 duration += entry.getCount() * entry.getDelta(); 536 } 537 return duration; 538 } 539 540 public long getTimescale(Movie movie) { 541 long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale(); 542 for (Track track : movie.getTracks()) { 543 timescale = gcd(track.getTrackMetaData().getTimescale(), timescale); 544 } 545 return timescale; 546 } 547 548 public static long gcd(long a, long b) { 549 if (b == 0) { 550 return a; 551 } 552 return gcd(b, a % b); 553 } 554 555 public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) { 556 ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size()); 557 for (ByteBuffer buffer : samples) { 558 int lastIndex = nuSamples.size() - 1; 559 if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() && 560 nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) { 561 ByteBuffer oldBuffer = nuSamples.remove(lastIndex); 562 ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice(); 563 // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset. 564 nuSamples.add(nu); 565 } else if (lastIndex >= 0 && 566 buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer && 567 nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) { 568 // This can go wrong - but will it? 569 ByteBuffer oldBuffer = nuSamples.get(lastIndex); 570 oldBuffer.limit(buffer.limit() + oldBuffer.limit()); 571 } else { 572 nuSamples.add(buffer); 573 } 574 } 575 return nuSamples; 576 } 577 } 578