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.*; 22import com.coremedia.iso.boxes.fragment.*; 23import com.googlecode.mp4parser.authoring.DateHelper; 24import com.googlecode.mp4parser.authoring.Movie; 25import com.googlecode.mp4parser.authoring.Track; 26 27import java.io.IOException; 28import java.nio.ByteBuffer; 29import java.nio.channels.GatheringByteChannel; 30import java.nio.channels.ReadableByteChannel; 31import java.nio.channels.WritableByteChannel; 32import java.util.*; 33import java.util.logging.Logger; 34 35import static com.googlecode.mp4parser.util.CastUtils.l2i; 36 37/** 38 * Creates a fragmented MP4 file. 39 */ 40public class FragmentedMp4Builder implements Mp4Builder { 41 private static final Logger LOG = Logger.getLogger(FragmentedMp4Builder.class.getName()); 42 43 protected FragmentIntersectionFinder intersectionFinder; 44 45 public FragmentedMp4Builder() { 46 this.intersectionFinder = new SyncSampleIntersectFinderImpl(); 47 } 48 49 public List<String> getAllowedHandlers() { 50 return Arrays.asList("soun", "vide"); 51 } 52 53 public Box createFtyp(Movie movie) { 54 List<String> minorBrands = new LinkedList<String>(); 55 minorBrands.add("isom"); 56 minorBrands.add("iso2"); 57 minorBrands.add("avc1"); 58 return new FileTypeBox("isom", 0, minorBrands); 59 } 60 61 /** 62 * Some formats require sorting of the fragments. E.g. Ultraviolet CFF files are required 63 * to contain the fragments size sort: 64 * <ul> 65 * <li>video[1].getBytes().length < audio[1].getBytes().length < subs[1].getBytes().length</li> 66 * <li> audio[2].getBytes().length < video[2].getBytes().length < subs[2].getBytes().length</li> 67 * </ul> 68 * 69 * make this fragment: 70 * 71 * <ol> 72 * <li>video[1]</li> 73 * <li>audio[1]</li> 74 * <li>subs[1]</li> 75 * <li>audio[2]</li> 76 * <li>video[2]</li> 77 * <li>subs[2]</li> 78 * </ol> 79 * 80 * @param tracks the list of tracks to returned sorted 81 * @param cycle current fragment (sorting may vary between the fragments) 82 * @param intersectionMap a map from tracks to their fragments' first samples. 83 * @return the list of tracks in order of appearance in the fragment 84 */ 85 protected List<Track> sortTracksInSequence(List<Track> tracks, final int cycle, final Map<Track, long[]> intersectionMap) { 86 tracks = new LinkedList<Track>(tracks); 87 Collections.sort(tracks, new Comparator<Track>() { 88 public int compare(Track o1, Track o2) { 89 long[] startSamples1 = intersectionMap.get(o1); 90 long startSample1 = startSamples1[cycle]; 91 // one based sample numbers - the first sample is 1 92 long endSample1 = cycle + 1 < startSamples1.length ? startSamples1[cycle + 1] : o1.getSamples().size() + 1; 93 long[] startSamples2 = intersectionMap.get(o2); 94 long startSample2 = startSamples2[cycle]; 95 // one based sample numbers - the first sample is 1 96 long endSample2 = cycle + 1 < startSamples2.length ? startSamples2[cycle + 1] : o2.getSamples().size() + 1; 97 List<ByteBuffer> samples1 = o1.getSamples().subList(l2i(startSample1) - 1, l2i(endSample1) - 1); 98 List<ByteBuffer> samples2 = o2.getSamples().subList(l2i(startSample2) - 1, l2i(endSample2) - 1); 99 int size1 = 0; 100 for (ByteBuffer byteBuffer : samples1) { 101 size1 += byteBuffer.limit(); 102 } 103 int size2 = 0; 104 for (ByteBuffer byteBuffer : samples2) { 105 size2 += byteBuffer.limit(); 106 } 107 return size1 - size2; 108 } 109 }); 110 return tracks; 111 } 112 113 protected List<Box> createMoofMdat(final Movie movie) { 114 List<Box> boxes = new LinkedList<Box>(); 115 HashMap<Track, long[]> intersectionMap = new HashMap<Track, long[]>(); 116 int maxNumberOfFragments = 0; 117 for (Track track : movie.getTracks()) { 118 long[] intersects = intersectionFinder.sampleNumbers(track, movie); 119 intersectionMap.put(track, intersects); 120 maxNumberOfFragments = Math.max(maxNumberOfFragments, intersects.length); 121 } 122 123 124 int sequence = 1; 125 // this loop has two indices: 126 127 for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) { 128 129 final List<Track> sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap); 130 131 for (Track track : sortedTracks) { 132 if (getAllowedHandlers().isEmpty() || getAllowedHandlers().contains(track.getHandler())) { 133 long[] startSamples = intersectionMap.get(track); 134 //some tracks may have less fragments -> skip them 135 if (cycle < startSamples.length) { 136 137 long startSample = startSamples[cycle]; 138 // one based sample numbers - the first sample is 1 139 long endSample = cycle + 1 < startSamples.length ? startSamples[cycle + 1] : track.getSamples().size() + 1; 140 141 // if startSample == endSample the cycle is empty! 142 if (startSample != endSample) { 143 boxes.add(createMoof(startSample, endSample, track, sequence)); 144 boxes.add(createMdat(startSample, endSample, track, sequence++)); 145 } 146 } 147 } 148 } 149 } 150 return boxes; 151 } 152 153 /** 154 * {@inheritDoc} 155 */ 156 public IsoFile build(Movie movie) { 157 LOG.fine("Creating movie " + movie); 158 IsoFile isoFile = new IsoFile(); 159 160 161 isoFile.addBox(createFtyp(movie)); 162 isoFile.addBox(createMoov(movie)); 163 164 for (Box box : createMoofMdat(movie)) { 165 isoFile.addBox(box); 166 } 167 isoFile.addBox(createMfra(movie, isoFile)); 168 169 return isoFile; 170 } 171 172 protected Box createMdat(final long startSample, final long endSample, final Track track, final int i) { 173 174 class Mdat implements Box { 175 ContainerBox parent; 176 177 public ContainerBox getParent() { 178 return parent; 179 } 180 181 public void setParent(ContainerBox parent) { 182 this.parent = parent; 183 } 184 185 public long getSize() { 186 long size = 8; // I don't expect 2gig fragments 187 for (ByteBuffer sample : getSamples(startSample, endSample, track, i)) { 188 size += sample.limit(); 189 } 190 return size; 191 } 192 193 public String getType() { 194 return "mdat"; 195 } 196 197 public void getBox(WritableByteChannel writableByteChannel) throws IOException { 198 List<ByteBuffer> bbs = getSamples(startSample, endSample, track, i); 199 final List<ByteBuffer> samples = ByteBufferHelper.mergeAdjacentBuffers(bbs); 200 ByteBuffer header = ByteBuffer.allocate(8); 201 IsoTypeWriter.writeUInt32(header, l2i(getSize())); 202 header.put(IsoFile.fourCCtoBytes(getType())); 203 header.rewind(); 204 writableByteChannel.write(header); 205 if (writableByteChannel instanceof GatheringByteChannel) { 206 207 int STEPSIZE = 1024; 208 // This is required to prevent android from crashing 209 // it seems that {@link GatheringByteChannel#write(java.nio.ByteBuffer[])} 210 // just handles up to 1024 buffers 211 for (int i = 0; i < Math.ceil((double) samples.size() / STEPSIZE); i++) { 212 List<ByteBuffer> sublist = samples.subList( 213 i * STEPSIZE, // start 214 (i + 1) * STEPSIZE < samples.size() ? (i + 1) * STEPSIZE : samples.size()); // end 215 ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]); 216 do { 217 ((GatheringByteChannel) writableByteChannel).write(sampleArray); 218 } while (sampleArray[sampleArray.length - 1].remaining() > 0); 219 } 220 //System.err.println(bytesWritten); 221 } else { 222 for (ByteBuffer sample : samples) { 223 sample.rewind(); 224 writableByteChannel.write(sample); 225 } 226 } 227 228 } 229 230 public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { 231 232 } 233 } 234 235 return new Mdat(); 236 } 237 238 protected Box createTfhd(long startSample, long endSample, Track track, int sequenceNumber) { 239 TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox(); 240 SampleFlags sf = new SampleFlags(); 241 242 tfhd.setDefaultSampleFlags(sf); 243 tfhd.setBaseDataOffset(-1); 244 tfhd.setTrackId(track.getTrackMetaData().getTrackId()); 245 return tfhd; 246 } 247 248 protected Box createMfhd(long startSample, long endSample, Track track, int sequenceNumber) { 249 MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox(); 250 mfhd.setSequenceNumber(sequenceNumber); 251 return mfhd; 252 } 253 254 protected Box createTraf(long startSample, long endSample, Track track, int sequenceNumber) { 255 TrackFragmentBox traf = new TrackFragmentBox(); 256 traf.addBox(createTfhd(startSample, endSample, track, sequenceNumber)); 257 for (Box trun : createTruns(startSample, endSample, track, sequenceNumber)) { 258 traf.addBox(trun); 259 } 260 261 return traf; 262 } 263 264 265 /** 266 * Gets the all samples starting with <code>startSample</code> (one based -> one is the first) and 267 * ending with <code>endSample</code> (exclusive). 268 * 269 * @param startSample low endpoint (inclusive) of the sample sequence 270 * @param endSample high endpoint (exclusive) of the sample sequence 271 * @param track source of the samples 272 * @param sequenceNumber the fragment index of the requested list of samples 273 * @return a <code>List<ByteBuffer></code> of raw samples 274 */ 275 protected List<ByteBuffer> getSamples(long startSample, long endSample, Track track, int sequenceNumber) { 276 // since startSample and endSample are one-based substract 1 before addressing list elements 277 return track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); 278 } 279 280 /** 281 * Gets the sizes of a sequence of samples- 282 * 283 * @param startSample low endpoint (inclusive) of the sample sequence 284 * @param endSample high endpoint (exclusive) of the sample sequence 285 * @param track source of the samples 286 * @param sequenceNumber the fragment index of the requested list of samples 287 * @return 288 */ 289 protected long[] getSampleSizes(long startSample, long endSample, Track track, int sequenceNumber) { 290 List<ByteBuffer> samples = getSamples(startSample, endSample, track, sequenceNumber); 291 292 long[] sampleSizes = new long[samples.size()]; 293 for (int i = 0; i < sampleSizes.length; i++) { 294 sampleSizes[i] = samples.get(i).limit(); 295 } 296 return sampleSizes; 297 } 298 299 /** 300 * Creates one or more track run boxes for a given sequence. 301 * 302 * @param startSample low endpoint (inclusive) of the sample sequence 303 * @param endSample high endpoint (exclusive) of the sample sequence 304 * @param track source of the samples 305 * @param sequenceNumber the fragment index of the requested list of samples 306 * @return the list of TrackRun boxes. 307 */ 308 protected List<? extends Box> createTruns(long startSample, long endSample, Track track, int sequenceNumber) { 309 TrackRunBox trun = new TrackRunBox(); 310 long[] sampleSizes = getSampleSizes(startSample, endSample, track, sequenceNumber); 311 312 trun.setSampleDurationPresent(true); 313 trun.setSampleSizePresent(true); 314 List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(l2i(endSample - startSample)); 315 316 317 Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries()); 318 long left = startSample - 1; 319 long curEntryLeft = timeQueue.peek().getCount(); 320 while (left > curEntryLeft) { 321 left -= curEntryLeft; 322 timeQueue.remove(); 323 curEntryLeft = timeQueue.peek().getCount(); 324 } 325 curEntryLeft -= left; 326 327 328 Queue<CompositionTimeToSample.Entry> compositionTimeQueue = 329 track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0 ? 330 new LinkedList<CompositionTimeToSample.Entry>(track.getCompositionTimeEntries()) : null; 331 long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue.peek().getCount() : -1; 332 333 334 trun.setSampleCompositionTimeOffsetPresent(compositionTimeEntriesLeft > 0); 335 336 // fast forward composition stuff 337 for (long i = 1; i < startSample; i++) { 338 if (compositionTimeQueue != null) { 339 //trun.setSampleCompositionTimeOffsetPresent(true); 340 if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { 341 compositionTimeQueue.remove(); 342 compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); 343 } 344 } 345 } 346 347 boolean sampleFlagsRequired = (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() || 348 track.getSyncSamples() != null && track.getSyncSamples().length != 0); 349 350 trun.setSampleFlagsPresent(sampleFlagsRequired); 351 352 for (int i = 0; i < sampleSizes.length; i++) { 353 TrackRunBox.Entry entry = new TrackRunBox.Entry(); 354 entry.setSampleSize(sampleSizes[i]); 355 if (sampleFlagsRequired) { 356 //if (false) { 357 SampleFlags sflags = new SampleFlags(); 358 359 if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { 360 SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i); 361 sflags.setSampleDependsOn(e.getSampleDependsOn()); 362 sflags.setSampleIsDependedOn(e.getSampleIsDependentOn()); 363 sflags.setSampleHasRedundancy(e.getSampleHasRedundancy()); 364 } 365 if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { 366 // we have to mark non-sync samples! 367 if (Arrays.binarySearch(track.getSyncSamples(), startSample + i) >= 0) { 368 sflags.setSampleIsDifferenceSample(false); 369 sflags.setSampleDependsOn(2); 370 } else { 371 sflags.setSampleIsDifferenceSample(true); 372 sflags.setSampleDependsOn(1); 373 } 374 } 375 // i don't have sample degradation 376 entry.setSampleFlags(sflags); 377 378 } 379 380 entry.setSampleDuration(timeQueue.peek().getDelta()); 381 if (--curEntryLeft == 0 && timeQueue.size() > 1) { 382 timeQueue.remove(); 383 curEntryLeft = timeQueue.peek().getCount(); 384 } 385 386 if (compositionTimeQueue != null) { 387 entry.setSampleCompositionTimeOffset(compositionTimeQueue.peek().getOffset()); 388 if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) { 389 compositionTimeQueue.remove(); 390 compositionTimeEntriesLeft = compositionTimeQueue.element().getCount(); 391 } 392 } 393 entries.add(entry); 394 } 395 396 trun.setEntries(entries); 397 398 return Collections.singletonList(trun); 399 } 400 401 /** 402 * Creates a 'moof' box for a given sequence of samples. 403 * 404 * @param startSample low endpoint (inclusive) of the sample sequence 405 * @param endSample high endpoint (exclusive) of the sample sequence 406 * @param track source of the samples 407 * @param sequenceNumber the fragment index of the requested list of samples 408 * @return the list of TrackRun boxes. 409 */ 410 protected Box createMoof(long startSample, long endSample, Track track, int sequenceNumber) { 411 MovieFragmentBox moof = new MovieFragmentBox(); 412 moof.addBox(createMfhd(startSample, endSample, track, sequenceNumber)); 413 moof.addBox(createTraf(startSample, endSample, track, sequenceNumber)); 414 415 TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0); 416 firstTrun.setDataOffset(1); // dummy to make size correct 417 firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size 418 419 return moof; 420 } 421 422 /** 423 * Creates a single 'mvhd' movie header box for a given movie. 424 * 425 * @param movie the concerned movie 426 * @return an 'mvhd' box 427 */ 428 protected Box createMvhd(Movie movie) { 429 MovieHeaderBox mvhd = new MovieHeaderBox(); 430 mvhd.setVersion(1); 431 mvhd.setCreationTime(DateHelper.convert(new Date())); 432 mvhd.setModificationTime(DateHelper.convert(new Date())); 433 long movieTimeScale = movie.getTimescale(); 434 long duration = 0; 435 436 for (Track track : movie.getTracks()) { 437 long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale(); 438 if (tracksDuration > duration) { 439 duration = tracksDuration; 440 } 441 442 443 } 444 445 mvhd.setDuration(duration); 446 mvhd.setTimescale(movieTimeScale); 447 // find the next available trackId 448 long nextTrackId = 0; 449 for (Track track : movie.getTracks()) { 450 nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; 451 } 452 mvhd.setNextTrackId(++nextTrackId); 453 return mvhd; 454 } 455 456 /** 457 * Creates a fully populated 'moov' box with all child boxes. Child boxes are: 458 * <ul> 459 * <li>{@link #createMvhd(com.googlecode.mp4parser.authoring.Movie) mvhd}</li> 460 * <li>{@link #createMvex(com.googlecode.mp4parser.authoring.Movie) mvex}</li> 461 * <li>a {@link #createTrak(com.googlecode.mp4parser.authoring.Track, com.googlecode.mp4parser.authoring.Movie) trak} for every track</li> 462 * </ul> 463 * 464 * @param movie the concerned movie 465 * @return fully populated 'moov' 466 */ 467 protected Box createMoov(Movie movie) { 468 MovieBox movieBox = new MovieBox(); 469 470 movieBox.addBox(createMvhd(movie)); 471 movieBox.addBox(createMvex(movie)); 472 473 for (Track track : movie.getTracks()) { 474 movieBox.addBox(createTrak(track, movie)); 475 } 476 // metadata here 477 return movieBox; 478 479 } 480 481 /** 482 * Creates a 'tfra' - track fragment random access box for the given track with the isoFile. 483 * The tfra contains a map of random access points with time as key and offset within the isofile 484 * as value. 485 * 486 * @param track the concerned track 487 * @param isoFile the track is contained in 488 * @return a track fragment random access box. 489 */ 490 protected Box createTfra(Track track, IsoFile isoFile) { 491 TrackFragmentRandomAccessBox tfra = new TrackFragmentRandomAccessBox(); 492 tfra.setVersion(1); // use long offsets and times 493 List<TrackFragmentRandomAccessBox.Entry> offset2timeEntries = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); 494 List<Box> boxes = isoFile.getBoxes(); 495 long offset = 0; 496 long duration = 0; 497 for (Box box : boxes) { 498 if (box instanceof MovieFragmentBox) { 499 List<TrackFragmentBox> trafs = ((MovieFragmentBox) box).getBoxes(TrackFragmentBox.class); 500 for (int i = 0; i < trafs.size(); i++) { 501 TrackFragmentBox traf = trafs.get(i); 502 if (traf.getTrackFragmentHeaderBox().getTrackId() == track.getTrackMetaData().getTrackId()) { 503 // here we are at the offset required for the current entry. 504 List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); 505 for (int j = 0; j < truns.size(); j++) { 506 List<TrackFragmentRandomAccessBox.Entry> offset2timeEntriesThisTrun = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); 507 TrackRunBox trun = truns.get(j); 508 for (int k = 0; k < trun.getEntries().size(); k++) { 509 TrackRunBox.Entry trunEntry = trun.getEntries().get(k); 510 SampleFlags sf = null; 511 if (k == 0 && trun.isFirstSampleFlagsPresent()) { 512 sf = trun.getFirstSampleFlags(); 513 } else if (trun.isSampleFlagsPresent()) { 514 sf = trunEntry.getSampleFlags(); 515 } else { 516 List<MovieExtendsBox> mvexs = isoFile.getMovieBox().getBoxes(MovieExtendsBox.class); 517 for (MovieExtendsBox mvex : mvexs) { 518 List<TrackExtendsBox> trexs = mvex.getBoxes(TrackExtendsBox.class); 519 for (TrackExtendsBox trex : trexs) { 520 if (trex.getTrackId() == track.getTrackMetaData().getTrackId()) { 521 sf = trex.getDefaultSampleFlags(); 522 } 523 } 524 } 525 526 } 527 if (sf == null) { 528 throw new RuntimeException("Could not find any SampleFlags to indicate random access or not"); 529 } 530 if (sf.getSampleDependsOn() == 2) { 531 offset2timeEntriesThisTrun.add(new TrackFragmentRandomAccessBox.Entry( 532 duration, 533 offset, 534 i + 1, j + 1, k + 1)); 535 } 536 duration += trunEntry.getSampleDuration(); 537 } 538 if (offset2timeEntriesThisTrun.size() == trun.getEntries().size() && trun.getEntries().size() > 0) { 539 // Oooops every sample seems to be random access sample 540 // is this an audio track? I don't care. 541 // I just use the first for trun sample for tfra random access 542 offset2timeEntries.add(offset2timeEntriesThisTrun.get(0)); 543 } else { 544 offset2timeEntries.addAll(offset2timeEntriesThisTrun); 545 } 546 } 547 } 548 } 549 } 550 551 552 offset += box.getSize(); 553 } 554 tfra.setEntries(offset2timeEntries); 555 tfra.setTrackId(track.getTrackMetaData().getTrackId()); 556 return tfra; 557 } 558 559 /** 560 * Creates a 'mfra' - movie fragment random access box for the given movie in the given 561 * isofile. Uses {@link #createTfra(com.googlecode.mp4parser.authoring.Track, com.coremedia.iso.IsoFile)} 562 * to generate the child boxes. 563 * 564 * @param movie concerned movie 565 * @param isoFile concerned isofile 566 * @return a complete 'mfra' box 567 */ 568 protected Box createMfra(Movie movie, IsoFile isoFile) { 569 MovieFragmentRandomAccessBox mfra = new MovieFragmentRandomAccessBox(); 570 for (Track track : movie.getTracks()) { 571 mfra.addBox(createTfra(track, isoFile)); 572 } 573 574 MovieFragmentRandomAccessOffsetBox mfro = new MovieFragmentRandomAccessOffsetBox(); 575 mfra.addBox(mfro); 576 mfro.setMfraSize(mfra.getSize()); 577 return mfra; 578 } 579 580 protected Box createTrex(Movie movie, Track track) { 581 TrackExtendsBox trex = new TrackExtendsBox(); 582 trex.setTrackId(track.getTrackMetaData().getTrackId()); 583 trex.setDefaultSampleDescriptionIndex(1); 584 trex.setDefaultSampleDuration(0); 585 trex.setDefaultSampleSize(0); 586 SampleFlags sf = new SampleFlags(); 587 if ("soun".equals(track.getHandler())) { 588 // as far as I know there is no audio encoding 589 // where the sample are not self contained. 590 sf.setSampleDependsOn(2); 591 sf.setSampleIsDependedOn(2); 592 } 593 trex.setDefaultSampleFlags(sf); 594 return trex; 595 } 596 597 /** 598 * Creates a 'mvex' - movie extends box and populates it with 'trex' boxes 599 * by calling {@link #createTrex(com.googlecode.mp4parser.authoring.Movie, com.googlecode.mp4parser.authoring.Track)} 600 * for each track to generate them 601 * 602 * @param movie the source movie 603 * @return a complete 'mvex' 604 */ 605 protected Box createMvex(Movie movie) { 606 MovieExtendsBox mvex = new MovieExtendsBox(); 607 final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox(); 608 for (Track track : movie.getTracks()) { 609 final long trackDuration = getTrackDuration(movie, track); 610 if (mved.getFragmentDuration() < trackDuration) { 611 mved.setFragmentDuration(trackDuration); 612 } 613 } 614 mvex.addBox(mved); 615 616 for (Track track : movie.getTracks()) { 617 mvex.addBox(createTrex(movie, track)); 618 } 619 return mvex; 620 } 621 622 protected Box createTkhd(Movie movie, Track track) { 623 TrackHeaderBox tkhd = new TrackHeaderBox(); 624 tkhd.setVersion(1); 625 int flags = 0; 626 if (track.isEnabled()) { 627 flags += 1; 628 } 629 630 if (track.isInMovie()) { 631 flags += 2; 632 } 633 634 if (track.isInPreview()) { 635 flags += 4; 636 } 637 638 if (track.isInPoster()) { 639 flags += 8; 640 } 641 tkhd.setFlags(flags); 642 643 tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); 644 tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); 645 // We need to take edit list box into account in trackheader duration 646 // but as long as I don't support edit list boxes it is sufficient to 647 // just translate media duration to movie timescale 648 tkhd.setDuration(getTrackDuration(movie, track)); 649 tkhd.setHeight(track.getTrackMetaData().getHeight()); 650 tkhd.setWidth(track.getTrackMetaData().getWidth()); 651 tkhd.setLayer(track.getTrackMetaData().getLayer()); 652 tkhd.setModificationTime(DateHelper.convert(new Date())); 653 tkhd.setTrackId(track.getTrackMetaData().getTrackId()); 654 tkhd.setVolume(track.getTrackMetaData().getVolume()); 655 return tkhd; 656 } 657 658 private long getTrackDuration(Movie movie, Track track) { 659 return getDuration(track) * movie.getTimescale() / track.getTrackMetaData().getTimescale(); 660 } 661 662 protected Box createMdhd(Movie movie, Track track) { 663 MediaHeaderBox mdhd = new MediaHeaderBox(); 664 mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime())); 665 mdhd.setDuration(getDuration(track)); 666 mdhd.setTimescale(track.getTrackMetaData().getTimescale()); 667 mdhd.setLanguage(track.getTrackMetaData().getLanguage()); 668 return mdhd; 669 } 670 671 protected Box createStbl(Movie movie, Track track) { 672 SampleTableBox stbl = new SampleTableBox(); 673 674 stbl.addBox(track.getSampleDescriptionBox()); 675 stbl.addBox(new TimeToSampleBox()); 676 //stbl.addBox(new SampleToChunkBox()); 677 stbl.addBox(new StaticChunkOffsetBox()); 678 return stbl; 679 } 680 681 protected Box createMinf(Track track, Movie movie) { 682 MediaInformationBox minf = new MediaInformationBox(); 683 minf.addBox(track.getMediaHeaderBox()); 684 minf.addBox(createDinf(movie, track)); 685 minf.addBox(createStbl(movie, track)); 686 return minf; 687 } 688 689 protected Box createMdiaHdlr(Track track, Movie movie) { 690 HandlerBox hdlr = new HandlerBox(); 691 hdlr.setHandlerType(track.getHandler()); 692 return hdlr; 693 } 694 695 protected Box createMdia(Track track, Movie movie) { 696 MediaBox mdia = new MediaBox(); 697 mdia.addBox(createMdhd(movie, track)); 698 699 700 mdia.addBox(createMdiaHdlr(track, movie)); 701 702 703 mdia.addBox(createMinf(track, movie)); 704 return mdia; 705 } 706 707 protected Box createTrak(Track track, Movie movie) { 708 LOG.fine("Creating Track " + track); 709 TrackBox trackBox = new TrackBox(); 710 trackBox.addBox(createTkhd(movie, track)); 711 trackBox.addBox(createMdia(track, movie)); 712 return trackBox; 713 } 714 715 protected DataInformationBox createDinf(Movie movie, Track track) { 716 DataInformationBox dinf = new DataInformationBox(); 717 DataReferenceBox dref = new DataReferenceBox(); 718 dinf.addBox(dref); 719 DataEntryUrlBox url = new DataEntryUrlBox(); 720 url.setFlags(1); 721 dref.addBox(url); 722 return dinf; 723 } 724 725 public FragmentIntersectionFinder getFragmentIntersectionFinder() { 726 return intersectionFinder; 727 } 728 729 public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) { 730 this.intersectionFinder = intersectionFinder; 731 } 732 733 protected long getDuration(Track track) { 734 long duration = 0; 735 for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { 736 duration += entry.getCount() * entry.getDelta(); 737 } 738 return duration; 739 } 740 741 742} 743