diff --git a/dash.fragmencrypter/pom.xml b/dash.fragmencrypter/pom.xml new file mode 100644 index 0000000..9e04b46 --- /dev/null +++ b/dash.fragmencrypter/pom.xml @@ -0,0 +1,126 @@ + + + + com.castlabs.dash + dash.encrypt + 1.0-SNAPSHOT + + 4.0.0 + jar + dash.fragmencrypter + + + com.castlabs.dash.dashfragmenter.Main + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-release-plugin + 2.4.2 + + true + true + false + + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.8.1 + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.2 + + + package + + shade + + + + + com.castlabs.dash.dashfragmenter.Main + + + + + + + + + + + + + args4j + args4j + 2.0.26 + + + com.googlecode.mp4parser + isoparser + 1.0-RC-37 + + + commons-io + commons-io + 2.4 + + + commons-lang + commons-lang + 2.6 + + + com.castlabs.dash + dash.xsd + 1.0-SNAPSHOT + + + + + + 3rdparty + https://repository.castlabs.com/content/repositories/3rdparty + castLabs Software + + false + never + + + true + + + + releases + https://repository.castlabs.com/content/repositories/releases + castLabs Software + + false + never + + + true + + + + + diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Command.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Command.java new file mode 100644 index 0000000..2e933a6 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Command.java @@ -0,0 +1,8 @@ +package com.castlabs.dash.dashfragmenter; + +import java.io.IOException; + +public interface Command { + int run() throws IOException; + +} \ No newline at end of file diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Main.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Main.java new file mode 100644 index 0000000..a640cc7 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/Main.java @@ -0,0 +1,59 @@ +package com.castlabs.dash.dashfragmenter; + +import com.castlabs.dash.dashfragmenter.mp4todash.DashFileSet; +import com.castlabs.dash.dashfragmenter.mp4todash.DashFileSetEncrypt; +import com.castlabs.dash.dashfragmenter.mp4todash.MuxMp4; +import org.apache.commons.io.IOUtils; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.spi.SubCommand; +import org.kohsuke.args4j.spi.SubCommandHandler; +import org.kohsuke.args4j.spi.SubCommands; + +import java.io.IOException; + + +public class Main { + + public static final String TOOL; + + @Argument( + handler = SubCommandHandler.class, + required = true, + metaVar = "command", + usage = "Command required. Supported commands are: [dash, encrypt, mux]" + ) + @SubCommands({ + @SubCommand(name = "dash", impl = DashFileSet.class), + @SubCommand(name = "encrypt", impl = DashFileSetEncrypt.class), + @SubCommand(name = "mux", impl = MuxMp4.class) + }) + Command command; + + public static void main(String[] args) throws Exception { + System.out.println(TOOL); + Main m = new Main(); + CmdLineParser parser = new CmdLineParser(m); + try { + parser.parseArgument(args); + m.command.run(); + } catch (CmdLineException e) { + System.err.println(e.getMessage()); + System.exit(1023); + } + + } + + + static { + String tool; + try { + tool = IOUtils.toString(Main.class.getResourceAsStream("/tool.txt")); + } catch (IOException e) { + tool = "Could not determine version"; + } + TOOL = tool; + } + +} \ No newline at end of file diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/AbstractManifestWriter.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/AbstractManifestWriter.java new file mode 100644 index 0000000..7f0ea12 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/AbstractManifestWriter.java @@ -0,0 +1,108 @@ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.authoring.Track; +import mpegCenc2013.DefaultKIDAttribute; +import mpegDASHSchemaMPD2011.*; +import org.apache.xmlbeans.GDuration; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public abstract class AbstractManifestWriter { + + protected final Map> trackFamilies; + protected final Map trackContainer; + protected final Map trackBitrates; + protected final Map trackFilenames; + protected final Map trackKeyIds; + + public AbstractManifestWriter(Map> trackFamilies, + Map trackContainer, + Map trackBitrates, + Map trackFilenames, + Map trackKeyIds) { + + this.trackFamilies = trackFamilies; + this.trackContainer = trackContainer; + this.trackBitrates = trackBitrates; + this.trackFilenames = trackFilenames; + this.trackKeyIds = trackKeyIds; + } + + + public MPDDocument getManifest() throws IOException { + + MPDDocument mdd = MPDDocument.Factory.newInstance(); + MPDtype mpd = mdd.addNewMPD(); + PeriodType periodType = mpd.addNewPeriod(); + periodType.setId("0"); + periodType.setStart(new GDuration(1, 0, 0, 0, 0, 0, 0, BigDecimal.ZERO)); + + ProgramInformationType programInformationType = mpd.addNewProgramInformation(); + programInformationType.setMoreInformationURL("www.castLabs.com"); + + + createPeriod(periodType); + + + mpd.setProfiles("urn:mpeg:dash:profile:isoff-main:2011"); + mpd.setType(PresentationType.STATIC); // no mpd update strategy implemented yet, could be dynamic + mpd.setMinBufferTime(new GDuration(1, 0, 0, 0, 0, 0, 2, BigDecimal.ZERO)); + mpd.setMediaPresentationDuration(periodType.getDuration()); + + return mdd; + } + + protected AdaptationSetType createAdaptationSet(PeriodType periodType, List tracks) { + UUID keyId = null; + String language = null; + for (Track track : tracks) { + if (keyId != null && !keyId.equals(trackKeyIds.get(track))) { + throw new RuntimeException("The ManifestWriter cannot deal with more than ONE key per adaptation set."); + } + keyId = trackKeyIds.get(track); + + if (language != null && !language.endsWith(track.getTrackMetaData().getLanguage())) { + throw new RuntimeException("The ManifestWriter cannot deal with more than ONE language per adaptation set."); + } + language = track.getTrackMetaData().getLanguage(); + } + + + AdaptationSetType adaptationSet = periodType.addNewAdaptationSet(); + adaptationSet.setSegmentAlignment(true); + adaptationSet.setStartWithSAP(1); + adaptationSet.setLang(language); + adaptationSet.setBitstreamSwitching(true); + + if (keyId != null) { + DescriptorType contentProtection = adaptationSet.addNewContentProtection(); + final DefaultKIDAttribute defaultKIDAttribute = DefaultKIDAttribute.Factory.newInstance(); + defaultKIDAttribute.setDefaultKID(Collections.singletonList(keyId.toString())); + contentProtection.set(defaultKIDAttribute); + contentProtection.setSchemeIdUri("urn:mpeg:dash:mp4protection:2011"); + contentProtection.setValue("cenc"); + } + return adaptationSet; + } + + protected void createInitialization(URLType urlType, Track track) { + long offset = 0; + for (Box box : trackContainer.get(track).getBoxes()) { + if ("moov".equals(box.getType())) { + urlType.setRange(String.format("%s-%s", offset, offset + box.getSize() - 1)); + break; + } + offset += box.getSize(); + } + } + + abstract protected void createPeriod(PeriodType periodType) throws IOException; + +} diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashBuilder.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashBuilder.java new file mode 100644 index 0000000..6100da1 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashBuilder.java @@ -0,0 +1,213 @@ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.EditListBox; +import com.coremedia.iso.boxes.FileTypeBox; +import com.coremedia.iso.boxes.fragment.MovieFragmentBox; +import com.coremedia.iso.boxes.fragment.TrackRunBox; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.FragmentedMp4Builder; +import com.googlecode.mp4parser.boxes.threegpp26244.SegmentIndexBox; + +import java.util.*; + +/** + * Creates a fragmented Dash conforming MP4 file. + */ +public class DashBuilder extends FragmentedMp4Builder { + + public DashBuilder() { + } + + @Override + protected List createMoofMdat(Movie movie) { + List moofsMdats = new LinkedList(); + HashMap intersectionMap = new HashMap(); + + int maxNumberOfFragments = 0; + for (Track track : movie.getTracks()) { + long[] intersects = intersectionFinder.sampleNumbers(track); + intersectionMap.put(track, intersects); + maxNumberOfFragments = Math.max(maxNumberOfFragments, intersects.length); + } + + + int sequence = 1; + // this loop has two indices: + + for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) { + + final List sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap); + + for (Track track : sortedTracks) { + if (getAllowedHandlers().isEmpty() || getAllowedHandlers().contains(track.getHandler())) { + long[] startSamples = intersectionMap.get(track); + sequence = createFragment(moofsMdats, track, startSamples, cycle, sequence); + } + } + } + + List sidx_boxes = new LinkedList(); + + int inserter = 0; + List newboxes = new ArrayList(); + int counter = 0; + SegmentIndexBox sidx = new SegmentIndexBox(); + + for (int i = 0; i < moofsMdats.size(); i++) { + + if (moofsMdats.get(i).getType().equals("sidx")) { + sidx_boxes.add((SegmentIndexBox) moofsMdats.get(i)); + counter++; + if (counter == 1) { + inserter = i; + } + } else { + newboxes.add(moofsMdats.get(i)); + } + } + + sidx.setEarliestPresentationTime(sidx_boxes.get(0).getEarliestPresentationTime()); + sidx.setFirstOffset(sidx_boxes.get(0).getFirstOffset()); + sidx.setReferenceId(sidx_boxes.get(0).getReferenceId()); + sidx.setTimeScale(sidx_boxes.get(0).getTimeScale()); + sidx.setFlags(sidx_boxes.get(0).getFlags()); + List sidxbox_entries = new ArrayList(); + for (SegmentIndexBox sidxbox : sidx_boxes) { + List entryfrag = sidxbox.getEntries(); + for (SegmentIndexBox.Entry entry : entryfrag) { + sidxbox_entries.add(entry); + } + } + + sidx.setEntries(sidxbox_entries); + newboxes.add(inserter, sidx); + return newboxes; + } + + @Override + protected int createFragment(List moofsMdats, Track track, long[] startSamples, int cycle, int sequence) { + List moofMdat = new ArrayList(); + int newSequence = super.createFragment(moofMdat, track, startSamples, cycle, sequence); + + if (moofMdat.isEmpty()) return newSequence; + + final MovieFragmentBox moof = (MovieFragmentBox) moofMdat.get(0); + final TrackRunBox trun = moof.getTrackRunBoxes().get(0); + final long[] ptss = getPtss(trun); + final int firstFrameSapType = getFirstFrameSapType(ptss); + long firstOffset = 0; + int referencedSize = (int) (moof.getSize() + moofMdat.get(1).getSize()); + long timeMappingEdit = getTimeMappingEditTime(track); + final Box sidx = createSidx(track, ptss[0] - timeMappingEdit, firstOffset, referencedSize, getSegmentDuration(trun), (byte) firstFrameSapType, 0); + + moofsMdats.add(sidx); + moofsMdats.addAll(moofMdat); + + return newSequence; + } + + private long getTimeMappingEditTime(Track track) { + final EditListBox editList = track.getTrackMetaData().getEditList(); + if (editList != null) { + final List entries = editList.getEntries(); + for (EditListBox.Entry entry : entries) { + if (entry.getMediaTime() > 0) { + return entry.getMediaTime(); + } + } + } + return 0; + } + + + protected long getSegmentDuration(TrackRunBox trun) { + final List trunEntries = trun.getEntries(); + long duration = 0; + for (TrackRunBox.Entry trunEntry : trunEntries) { + duration += trunEntry.getSampleDuration(); + } + return duration; + } + + protected int getFirstFrameSapType(long[] ptss) { + long idrTimeStamp = ptss[0]; + Arrays.sort(ptss); + if (idrTimeStamp > ptss[0]) { + return 0; + } else { + return 1; + } + } + + private long[] getPtss(TrackRunBox trun) { + long currentTime = 0; + long[] ptss = new long[trun.getEntries().size()]; + for (int j = 0; j < ptss.length; j++) { + ptss[j] = currentTime + trun.getEntries().get(j).getSampleCompositionTimeOffset(); + currentTime += trun.getEntries().get(j).getSampleDuration(); + } + return ptss; + } + + + /** + * Creates a 'sidx' box + */ + protected Box createSidx(Track track, long earliestPresentationTime, long firstOffset, int referencedSize, long subSegmentDuration, byte sap, int sapDelta) { + SegmentIndexBox sidx = new SegmentIndexBox(); + + sidx.setEarliestPresentationTime(earliestPresentationTime); + sidx.setFirstOffset(firstOffset); + sidx.setReferenceId(track.getTrackMetaData().getTrackId()); + sidx.setTimeScale(track.getTrackMetaData().getTimescale()); + sidx.setFlags(0); + sidx.setReserved(0); + SegmentIndexBox.Entry sidxentry = createSidxEntry(referencedSize, subSegmentDuration, sap, sapDelta); + + ArrayList sidxEntries = new ArrayList(); + sidxEntries.add(sidxentry); + sidx.setEntries(sidxEntries); + return sidx; + } + + private SegmentIndexBox.Entry createSidxEntry(int referencedSize, long subSegmentDuration, byte sapType, int sapDelta) { + SegmentIndexBox.Entry sidxentry = new SegmentIndexBox.Entry(); + byte referenceType = 0; //media + byte startWithSAP; + int sapDeltaTime; + if (sapType == 1) { + startWithSAP = 1; // fragments are cut at I-Frames + sapDeltaTime = 0; + } else { + //todo fix + //if (true) throw new RuntimeException("can't handle other than sap_type 1 properly"); + startWithSAP = 0; + sapDeltaTime = sapDelta; + } + sidxentry.setReferenceType(referenceType); + sidxentry.setReferencedSize(referencedSize); + sidxentry.setSubsegmentDuration(subSegmentDuration); + sidxentry.setStartsWithSap(startWithSAP); + sidxentry.setSapType(sapType); + sidxentry.setSapDeltaTime(sapDeltaTime); + return sidxentry; + } + + @Override + public Box createFtyp(Movie movie) { + List minorBrands = new LinkedList(); + minorBrands.add("mp42"); + minorBrands.add("dash"); + minorBrands.add("msdh"); + minorBrands.add("msix"); + minorBrands.add("iso5"); + minorBrands.add("avc1"); + minorBrands.add("isom"); + + return new FileTypeBox("iso6", 1, minorBrands); + } + + +} diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashEncryptedBuilder.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashEncryptedBuilder.java new file mode 100644 index 0000000..9fe03ad --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashEncryptedBuilder.java @@ -0,0 +1,348 @@ +/* + * Copyright 2012 Sebastian Annies, Hamburg + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.IsoTypeReaderVariable; +import com.coremedia.iso.boxes.*; +import com.coremedia.iso.boxes.fragment.TrackFragmentBox; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.coremedia.iso.boxes.sampleentry.SampleEntry; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Sample; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.boxes.AbstractSampleEncryptionBox; +import com.googlecode.mp4parser.boxes.basemediaformat.TrackEncryptionBox; +import com.googlecode.mp4parser.boxes.cenc.CencSampleAuxiliaryDataFormat; +import com.googlecode.mp4parser.boxes.cenc.CommonEncryptionSampleList; +import com.googlecode.mp4parser.boxes.cenc.ProtectionSystemSpecificHeaderBox; +import com.googlecode.mp4parser.boxes.ultraviolet.SampleEncryptionBox; + +import javax.crypto.SecretKey; +import java.lang.ref.SoftReference; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.*; + +import static com.googlecode.mp4parser.util.CastUtils.l2i; + +/** + * Creates a fragmented Dash conforming MP4 file. + */ +public class DashEncryptedBuilder extends DashBuilder { + boolean dummyIvs = false; // todo remove but at the moment needed to generate stable result + + private Map> sencCache = new WeakHashMap>(); + private Random random = new Random(5); + private Map keyIds = new HashMap(); + private Map keys = new HashMap(); + private Map psshBoxes; + private Map>> sampleListCache = new WeakHashMap>>(); + + public Map getKeyIds() { + return keyIds; + } + + public Map getKeys() { + return keys; + } + + public void setPsshBoxes(Map psshBoxes) { + this.psshBoxes = psshBoxes; + } + + + protected ProtectionSystemSpecificHeaderBox createProtectionSystemSpecificHeaderBox( + byte[] systemId, + byte[] content) { + + final ProtectionSystemSpecificHeaderBox pssh = new ProtectionSystemSpecificHeaderBox(); + pssh.setSystemId(systemId); + pssh.setContent(content); + + return pssh; + } + + protected List createIvs(long startSample, long endSample, Track track, int sequenceNumber) { + List ivs = new LinkedList(); + BigInteger one = new BigInteger("1"); + byte[] init = new byte[]{0, 0, 0, 0, 0, 0, 0, 0}; + + + if (!dummyIvs) { + random.nextBytes(init); + } + BigInteger ivInt = new BigInteger(1, init); + for (long i = startSample; i < endSample; i++) { + byte[] iv = ivInt.toByteArray(); + // iv must not always be 8 byte long. It could be shorter!!! + // or longer + byte[] eightByteIv = new byte[]{0, 0, 0, 0, 0, 0, 0, 0}; + System.arraycopy( + iv, + iv.length - 8 > 0 ? iv.length - 8 : 0, + eightByteIv, + (8 - iv.length) < 0 ? 0 : (8 - iv.length), + iv.length > 8 ? 8 : iv.length); + ivs.add(eightByteIv); + ivInt = ivInt.add(one); + } + + + return ivs; + } + + + @Override + protected Box createTraf(long startSample, long endSample, Track track, int sequenceNumber) { + TrackFragmentBox traf = new TrackFragmentBox(); + traf.addBox(createTfhd(startSample, endSample, track, sequenceNumber)); + traf.addBox(createTfdt(startSample, track)); + traf.addBox(createTrun(startSample, endSample, track, sequenceNumber)); + + SampleEncryptionBox senc = (SampleEncryptionBox) createSenc(startSample, endSample, track, sequenceNumber); + List sizes = senc.getEntrySizes(); + + // SAIZ + int samplecount = (int) (endSample - startSample); + int flags = 1; + String auxInfoType = "cenc"; + String auxInfoTypeParameter = "\0\0\0\0"; + + //todo - if one value in sizes varies, then default is to be set to 0 and the list accordingly + traf.addBox(createSaiz(samplecount, flags, 0, sizes, auxInfoType, auxInfoTypeParameter)); + traf.addBox(senc); + + + //SAIO + + + // moof.getSize() + mfhd.getSize() + long offset = 40; // moof hdr + mfhd hdr + traf hdr + + for (Box box : traf.getBoxes()) { + if (box instanceof SampleEncryptionBox) { + offset += ((SampleEncryptionBox) box).getOffsetToFirstIV(); + break; + } + offset += box.getSize(); + } + + traf.addBox(createSaio(flags, auxInfoType, auxInfoTypeParameter, Collections.singletonList(offset))); + return traf; + } + + + protected AbstractSampleEncryptionBox createSenc(long startSample, long endSample, Track track, int sequenceNumber) { + Map trackSpecificCache = sencCache.get(track); + if (trackSpecificCache == null) { + trackSpecificCache = new HashMap(); + sencCache.put(track, trackSpecificCache); + } + SampleEncryptionBox senc = trackSpecificCache.get(sequenceNumber); + if (senc != null) { + return senc; + } + senc = new SampleEncryptionBox(); + + List entries = new LinkedList(); + LinkedList iVStack = new LinkedList(createIvs(startSample, endSample, track, sequenceNumber)); + + LinkedList samples = new LinkedList(track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1)); + AvcConfigurationBox avcC = null; + List boxes = track.getSampleDescriptionBox().getSampleEntry().getBoxes(); + for (Box box : boxes) { + if (box instanceof AvcConfigurationBox) { + avcC = (AvcConfigurationBox) box; + senc.setSubSampleEncryption(true); + } + } + + while (iVStack.size() > 0 && samples.size() > 0) { + + CencSampleAuxiliaryDataFormat e = new CencSampleAuxiliaryDataFormat(); + entries.add(e); + e.iv = iVStack.removeFirst(); + ByteBuffer sample = (ByteBuffer) samples.remove().asByteBuffer().rewind(); + + + if (avcC != null) { + int nalLengthSize = avcC.getLengthSizeMinusOne() + 1; + while (sample.remaining() > 0) { + int nalLength = l2i(IsoTypeReaderVariable.read(sample, nalLengthSize)); + int clearBytes; + int nalGrossSize = nalLength + nalLengthSize; + if (nalGrossSize >= 112) { + clearBytes = 96 + nalGrossSize % 16; + } else { + clearBytes = nalGrossSize; + } + e.pairs.add(e.createPair(clearBytes, nalGrossSize - clearBytes)); + sample.position(sample.position() + nalLength); + } + } + } + senc.setEntries(entries); + trackSpecificCache.put(sequenceNumber, senc); + return senc; + } + + private Box createSaiz(int samplecount, int flags, int defaultSampleInfoSize, List sampleInforSizes, String auxInfoType, String auxInfoTypeParameter) { + SampleAuxiliaryInformationSizesBox saiz = new SampleAuxiliaryInformationSizesBox(); + saiz.setFlags(flags); + saiz.setAuxInfoType(auxInfoType); + saiz.setAuxInfoTypeParameter(auxInfoTypeParameter); + saiz.setDefaultSampleInfoSize(defaultSampleInfoSize); + saiz.setSampleInfoSizes(sampleInforSizes); + saiz.setSampleCount(samplecount); + return saiz; + } + + private Box createSaio(int flags, String auxInfoType, String auxInfoTypeParameter, List offsets) { + SampleAuxiliaryInformationOffsetsBox saio = new SampleAuxiliaryInformationOffsetsBox(); + saio.setFlags(flags); + saio.setAuxInfoType(auxInfoType); + saio.setAuxInfoTypeParameter(auxInfoTypeParameter); + saio.setOffsets(offsets); + + return saio; + } + + /** + * Creates a fully populated 'moov' box with all child boxes. Child boxes are: + *
    + *
  • {@link #createMvhd(com.googlecode.mp4parser.authoring.Movie) mvhd}
  • + *
  • {@link #createMvex(com.googlecode.mp4parser.authoring.Movie) mvex}
  • + *
  • a {@link #createTrak(com.googlecode.mp4parser.authoring.Track, com.googlecode.mp4parser.authoring.Movie) trak} for every track
  • + *
+ * + * @param movie the concerned movie + * @return fully populated 'moov' + */ + @Override + protected Box createMoov(Movie movie) { + MovieBox movieBox = new MovieBox(); + + movieBox.addBox(createMvhd(movie)); + + for (byte[] systemIdBytes : psshBoxes.keySet()) { + movieBox.addBox(createProtectionSystemSpecificHeaderBox(systemIdBytes, psshBoxes.get(systemIdBytes))); + } + + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrak(track, movie)); + } + + movieBox.addBox(createMvex(movie)); + + // metadata here + return movieBox; + + } + + /** + * Gets all samples starting with startSample (one based -> one is the first) and + * ending with endSample (exclusive). + * + * @param startSample low endpoint (inclusive) of the sample sequence + * @param endSample high endpoint (exclusive) of the sample sequence + * @param track source of the samples + * @param sequenceNumber the fragment index of the requested list of samples + * @return a List<ByteBuffer> of raw samples + */ + @Override + protected List getSamples(long startSample, long endSample, Track track, int sequenceNumber) { + final SoftReference> listSoftReference = sampleListCache.get(sequenceNumber); + if (listSoftReference != null && listSoftReference.get() != null) { + return listSoftReference.get(); + } + // since startSample and endSample are one-based substract 1 before addressing list elements + final List samples = track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); + + SecretKey secretKey = keys.get(track); + if (secretKey == null) { + System.err.println("Couldn't find key for track " + track + " with handler " + track.getHandler()); + return samples; + } + + final List cencEntries = createSenc(startSample, endSample, track, sequenceNumber).getEntries(); + final CommonEncryptionSampleList sampleList = new CommonEncryptionSampleList(secretKey, + samples, + cencEntries); + + sampleListCache.put(sequenceNumber, new SoftReference>(sampleList)); + return sampleList; + } + + protected SchemeTypeBox createSchm() { + SchemeTypeBox schemeTypeBox = new SchemeTypeBox(); + schemeTypeBox.setSchemeVersion(0x00010000); + schemeTypeBox.setSchemeType("cenc"); + return schemeTypeBox; + } + + protected Box createSchi(Track track) { + SchemeInformationBox schemeInformationBox = new SchemeInformationBox(); + TrackEncryptionBox trackEncryptionBox = new TrackEncryptionBox(); + trackEncryptionBox.setDefaultIvSize(8); + trackEncryptionBox.setDefaultAlgorithmId(0x01); + trackEncryptionBox.setDefault_KID(keyIds.get(track)); + schemeInformationBox.addBox(trackEncryptionBox); + return schemeInformationBox; + } + + @Override + protected Box createStbl(Movie movie, Track track) { + SampleTableBox stbl = new SampleTableBox(); + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + + SampleEntry sampleEntry = stsd.getSampleEntry(); + // Apple often has extra boxes next to avcc + // remvoe them to become spec compliant + + if (keys.get(track) != null) { + OriginalFormatBox originalFormatBox = new OriginalFormatBox(); + originalFormatBox.setDataFormat(sampleEntry.getType()); + ProtectionSchemeInformationBox protectionSchemeInformationBox = new ProtectionSchemeInformationBox(); + protectionSchemeInformationBox.addBox(originalFormatBox); + protectionSchemeInformationBox.addBox(createSchm()); + + if (sampleEntry instanceof AudioSampleEntry) { + ((AudioSampleEntry) sampleEntry).setType(AudioSampleEntry.TYPE_ENCRYPTED); + ((AudioSampleEntry) sampleEntry).addBox(protectionSchemeInformationBox); + } + if (sampleEntry instanceof VisualSampleEntry && sampleEntry.getType().equals("avc1")) { + ((VisualSampleEntry) sampleEntry).setCompressorname("AVC Coding"); + ((VisualSampleEntry) sampleEntry).setType(VisualSampleEntry.TYPE_ENCRYPTED); + ((VisualSampleEntry) sampleEntry).addBox(protectionSchemeInformationBox); + } + + protectionSchemeInformationBox.addBox(createSchi(track)); + + stbl.addBox(stsd); + } else { + System.err.println("Couldn't find key for track " + track + " with handler " + track.getHandler()); + stbl.addBox(track.getSampleDescriptionBox()); + } + stbl.addBox(new TimeToSampleBox()); + stbl.addBox(new SampleToChunkBox()); + stbl.addBox(new SampleSizeBox()); + stbl.addBox(new StaticChunkOffsetBox()); + return stbl; + } + +} diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSet.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSet.java new file mode 100644 index 0000000..123a9af --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSet.java @@ -0,0 +1,349 @@ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.castlabs.dash.dashfragmenter.Command; +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.FileDataSourceImpl; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Sample; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.*; +import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; +import com.googlecode.mp4parser.authoring.tracks.AACTrackImpl; +import com.googlecode.mp4parser.authoring.tracks.AC3TrackImpl; +import com.googlecode.mp4parser.authoring.tracks.DTSTrackImpl; +import com.googlecode.mp4parser.authoring.tracks.EC3TrackImpl; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig; +import mpegDASHSchemaMPD2011.MPDDocument; +import org.apache.xmlbeans.XmlOptions; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.FileOptionHandler; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.WritableByteChannel; +import java.util.*; + + +public class DashFileSet implements Command { + static Set supportedTypes = new HashSet(Arrays.asList("ac-3", "ec-3", "dtsl", "dtsh", "dtse", "avc1", "mp4a")); + + @Argument(required = true, multiValued = true, handler = FileOptionHandler.class, usage = "MP4 and bitstream input files", metaVar = "vid1.mp4, vid2.mp4, aud1.mp4, aud2.ec3 ...") + protected List inputFiles; + + @Option(name = "--outputdir", aliases = "-o", + usage = "output directory - if no output directory is given the " + + "current working directory is used.", + metaVar = "PATH") + protected File outputDirectory = new File(""); + + + @Override + public int run() throws IOException { + if (!(outputDirectory.exists() ^ outputDirectory.mkdirs())) { + System.err.println("Output directory does not exist and cannot be created."); + } + + long start = System.currentTimeMillis(); + + Map trackOriginalFilename = createTracks(); + + // sort by language and codec + Map> trackFamilies = findTrackFamilies(trackOriginalFilename.keySet()); + + // Track sizes are expensive to calculate -> save them for later + Map trackSizes = calculateTrackSizes(trackFamilies); + + // sort within the track families by size to get stable output + sortTrackFamilies(trackFamilies, trackSizes); + + // calculate the fragment start samples once & save them for later + Map trackStartSamples = findFragmentStartSamples(trackFamilies); + + // calculate bitrates + Map trackBitrate = calculateBitrate(trackFamilies, trackSizes); + + // generate filenames for later reference + Map trackFilename = generateFilenames(trackOriginalFilename); + + // export the dashed single track MP4s + Map dashedFiles = writeSingleTrackDashedMp4s(trackStartSamples, trackFilename); + + writeManifest(trackFamilies, trackBitrate, trackFilename, dashedFiles); + + System.out.println("Finished write in " + (System.currentTimeMillis() - start) + "ms"); + return 0; + } + + protected void writeManifest(Map> trackFamilies, + Map trackBitrate, + Map trackFilename, + Map dashedFiles) throws IOException { + MPDDocument mpdDocument1 = + new SegmentBaseSingleSidxManifestWriterImpl(trackFamilies, dashedFiles, trackBitrate, + trackFilename, Collections.emptyMap()).getManifest(); + + MPDDocument mpdDocument2 = + new SegmentListManifestWriterImpl(trackFamilies, dashedFiles, trackBitrate, + trackFilename, Collections.emptyMap()).getManifest(); + + XmlOptions xmlOptions = new XmlOptions(); + //xmlOptions.setUseDefaultNamespace(); + HashMap ns = new HashMap(); + //ns.put("urn:mpeg:DASH:schema:MPD:2011", ""); + ns.put("urn:mpeg:cenc:2013", "cenc"); + xmlOptions.setSaveSuggestedPrefixes(ns); + xmlOptions.setSaveAggressiveNamespaces(); + xmlOptions.setUseDefaultNamespace(); + xmlOptions.setSavePrettyPrint(); + File manifest1 = new File(outputDirectory, "Manifest.mpd"); + System.out.print("Writing " + manifest1 + "... "); + mpdDocument1.save(manifest1, xmlOptions); + System.out.println("Done."); + File manifest2 = new File(outputDirectory, "Manifest-segment-list.mpd"); + System.out.print("Writing " + manifest2 + "... "); + mpdDocument2.save(manifest2, xmlOptions); + System.out.println("Done."); + + } + + Mp4Builder getFileBuilder(FragmentIntersectionFinder fragmentIntersectionFinder, Movie m) { + DashBuilder dashBuilder = new DashBuilder(); + dashBuilder.setIntersectionFinder(fragmentIntersectionFinder); + return dashBuilder; + } + + private HashMap writeSingleTrackDashedMp4s( + Map fragmentStartSamples, + Map filenames) throws IOException { + + HashMap containers = new HashMap(); + for (final Map.Entry trackEntry : fragmentStartSamples.entrySet()) { + String filename = filenames.get(trackEntry.getKey()); + Movie movie = new Movie(); + movie.addTrack(trackEntry.getKey()); + + System.out.print("Creating model for " + filename + "... "); + Mp4Builder mp4Builder = getFileBuilder( + new StaticFragmentIntersectionFinderImpl(fragmentStartSamples), + movie); + Container isoFile = mp4Builder.build(movie); + + System.out.print("Writing... "); + WritableByteChannel wbc = new FileOutputStream( + new File(outputDirectory, filename)).getChannel(); + try { + isoFile.writeContainer(wbc); + containers.put(trackEntry.getKey(), isoFile); + } finally { + wbc.close(); + } + System.out.println("Done."); + } + return containers; + } + + private void sortTrackFamilies(Map> trackFamilies, final Map sizes) { + for (List tracks : trackFamilies.values()) { + Collections.sort(tracks, new Comparator() { + @Override + public int compare(Track o1, Track o2) { + return (int) (sizes.get(o1) - sizes.get(o2)); + } + }); + } + } + + /** + * Calculates approximate track size suitable for sorting & calculating bitrate but not suitable + * for precise calculations. + * + * @param trackFamilies all tracks grouped by their type. + * @return map from track to track's size + */ + private Map calculateTrackSizes(Map> trackFamilies) { + HashMap sizes = new HashMap(); + for (List tracks : trackFamilies.values()) { + for (Track track : tracks) { + long size = 0; + List samples = track.getSamples(); + for (int i = 0; i < Math.min(samples.size(), 10000); i++) { + size += samples.get(i).getSize(); + } + size = (size / Math.min(track.getSamples().size(), 10000)) * track.getSamples().size(); + sizes.put(track, size); + } + } + return sizes; + } + + /** + * Calculates bitrate from sizes. + * + * @param trackFamilies all tracks grouped by their type. + * @param trackSize size per track + * @return bitrate per track + */ + private Map calculateBitrate(Map> trackFamilies, Map trackSize) { + HashMap bitrates = new HashMap(); + for (List tracks : trackFamilies.values()) { + for (Track track : tracks) { + + double duration = (double) track.getDuration() / track.getTrackMetaData().getTimescale(); + long size = trackSize.get(track); + + bitrates.put(track, (long) ((size * 8 / duration / 1000)) * 1000); + } + + } + return bitrates; + } + + /** + * Generates filenames from type, language and bitrate. + * + * @return a descriptive filename type[-lang]-bitrate.mp4 + */ + private Map generateFilenames(Map trackOriginalFilename) { + HashMap filenames = new HashMap(); + for (Track track : trackOriginalFilename.keySet()) { + String originalFilename = trackOriginalFilename.get(track); + originalFilename = originalFilename.replace(".mp4", ""); + originalFilename = originalFilename.replace(".aac", ""); + originalFilename = originalFilename.replace(".ec3", ""); + originalFilename = originalFilename.replace(".ac3", ""); + originalFilename = originalFilename.replace(".dtshd", ""); + for (Track track1 : filenames.keySet()) { + if (track1 != track && + trackOriginalFilename.get(track1).equals(trackOriginalFilename.get(track))) { + // ouch multiple tracks point to same file + originalFilename += "_" + track.getTrackMetaData().getTrackId(); + } + } + filenames.put(track, String.format("%s.mp4", originalFilename)); + + } + return filenames; + } + + Map findFragmentStartSamples(Map> trackFamilies) { + Map fragmentStartSamples = new HashMap(); + + for (String key : trackFamilies.keySet()) { + List tracks = trackFamilies.get(key); + Movie movie = new Movie(); + movie.setTracks(tracks); + for (Track track : tracks) { + if (track.getHandler().startsWith("vide")) { + FragmentIntersectionFinder videoIntersectionFinder = new SyncSampleIntersectFinderImpl(movie, null, 2); + fragmentStartSamples.put(track, videoIntersectionFinder.sampleNumbers(track)); + //fragmentStartSamples.put(track, checkMaxFragmentDuration(track, videoIntersectionFinder.sampleNumbers(track))); + } else if (track.getHandler().startsWith("soun")) { + FragmentIntersectionFinder soundIntersectionFinder = new TwoSecondIntersectionFinder(movie, 5); + fragmentStartSamples.put(track, soundIntersectionFinder.sampleNumbers(track)); + } else { + throw new RuntimeException("An engineer needs to tell me if " + key + " is audio or video!"); + } + } + } + return fragmentStartSamples; + } + + private long[] checkMaxFragmentDuration(Track track, long[] sampleNumbers) { + ArrayList tweakedFragmentStartSampleNumbers = new ArrayList(sampleNumbers.length); + for (int i = 0; i < sampleNumbers.length - 1; i++) { + long startSampleNum = sampleNumbers[i]; + long endSampleNum = sampleNumbers[i + 1]; + long fragmentDuration = getFragmentDuration(track, startSampleNum, endSampleNum); + tweakedFragmentStartSampleNumbers.add(sampleNumbers[i]); + final long timescale = track.getTrackMetaData().getTimescale(); + if (fragmentDuration > 10 * timescale) { + System.err.println("Warning: fragment " + i + 1 + " has a duration of >10 sec (" + fragmentDuration / timescale + ")"); +// //insert fragment start sample in between the original fragment start samples + tweakedFragmentStartSampleNumbers.add(sampleNumbers[i] + + (sampleNumbers[i + 1] - sampleNumbers[i]) / 2); + } + } + long[] returnvalue = new long[tweakedFragmentStartSampleNumbers.size()]; + for (int i = 0; i < tweakedFragmentStartSampleNumbers.size(); i++) { + Long sampleNumber = tweakedFragmentStartSampleNumbers.get(i); + returnvalue[i] = sampleNumber; + } + return returnvalue; + } + + private long getFragmentDuration(Track track, long startSampleNum, long endSampleNum) { + final long[] sampleDurations = track.getSampleDurations(); + long fragmentDuration = 0; + for (int i = (int) startSampleNum - 1; i < endSampleNum - 1; i++) { + fragmentDuration += sampleDurations[i]; + } + return fragmentDuration; + } + + /** + * Creates a Map with Track as key and originating filename as value. + * + * @return Track too originating file map + * @throws IOException + */ + Map createTracks() throws IOException { + HashMap track2File = new HashMap(); + for (File inputFile : inputFiles) { + if (inputFile.getName().endsWith("mp4")) { + Movie movie = MovieCreator.build(new FileDataSourceImpl(inputFile)); + for (Track track : movie.getTracks()) { + String codec = track.getSampleDescriptionBox().getSampleEntry().getType(); + if (!supportedTypes.contains(codec)) { + System.out.println("Excluding " + inputFile + " track " + track.getTrackMetaData().getTrackId() + " as its codec " + codec + " is not yet supported"); + break; + } + track2File.put(track, inputFile.getName()); + } + } else if (inputFile.getName().endsWith(".aac")) { + Track track = new AACTrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".ac3")) { + Track track = new AC3TrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".ec3")) { + Track track = new EC3TrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".dtshd")) { + Track track = new DTSTrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else { + System.err.println("Cannot identify type of " + inputFile + ". Extensions mp4, aac, ac3, ec3 or dtshd are known."); + } + } + + return track2File; + + } + + Map> findTrackFamilies(Set allTracks) throws IOException { + HashMap> trackFamilies = new HashMap>(); + for (Track track : allTracks) { + String family = track.getSampleDescriptionBox().getSampleEntry().getType() + "-" + track.getTrackMetaData().getLanguage(); + if ("mp4a".equals(track.getSampleDescriptionBox().getSampleEntry().getType())) { + // we need to look at actual channel configuration + ESDescriptorBox esds = track.getSampleDescriptionBox().getSampleEntry().getBoxes(ESDescriptorBox.class).get(0); + AudioSpecificConfig audioSpecificConfig = esds.getEsDescriptor().getDecoderConfigDescriptor().getAudioSpecificInfo(); + family += "-" + audioSpecificConfig.getChannelConfiguration(); + } + + List tracks = trackFamilies.get(family); + if (tracks == null) { + tracks = new LinkedList(); + trackFamilies.put(family, tracks); + } + tracks.add(track); + } + + return trackFamilies; + } + + +} \ No newline at end of file diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSetEncrypt.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSetEncrypt.java new file mode 100644 index 0000000..20da88f --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/DashFileSetEncrypt.java @@ -0,0 +1,112 @@ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.Hex; +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder; +import com.googlecode.mp4parser.authoring.builder.Mp4Builder; +import com.googlecode.mp4parser.util.UUIDConverter; +import mpegDASHSchemaMPD2011.MPDDocument; +import org.apache.commons.io.FileUtils; +import org.apache.xmlbeans.XmlOptions; +import org.kohsuke.args4j.Option; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + + +public class DashFileSetEncrypt extends DashFileSet { + + UUID keyid; + SecretKey key; + + @Option(name = "--uuid", + aliases = "-u", + usage = "UUID (KeyID)" + ) + String encKid = null; + + @Option(name = "--secretKey", + aliases = "-k", + usage = "Secret Key (Key)", + depends = {"--uuid"} + + ) + String encKeySecretKey = null; + + @Option(name = "--secretKeyFile", + aliases = "-f", + usage = "Path to file", + depends = {"--uuid"} + ) + String encKeySecretKeyFile = null; + + @Override + public int run() throws IOException { + if (((this.encKeySecretKey == null) && (this.encKeySecretKeyFile == null))) { + System.out.println("Please specify --secretKey or --secretKeyFile "); + // --uuid requirements is done via cmd line parser + return 1236; + } else { + this.keyid = UUID.fromString(this.encKid); + if (this.encKeySecretKey != null) { + this.key = new SecretKeySpec(Hex.decodeHex(this.encKeySecretKey), "AES"); + } else { + this.key = new SecretKeySpec(Hex.decodeHex(FileUtils.readFileToString(new File(this.encKeySecretKeyFile))), "AES"); + } + } + super.run(); + + return 0; + } + + @Override + protected void writeManifest(Map> trackFamilies, Map trackBitrate, Map trackFilename, Map dashedFiles) throws IOException { + + Map trackKeyIds = new HashMap(); + for (List tracks : trackFamilies.values()) { + for (Track track : tracks) { + trackKeyIds.put(track, this.keyid); + } + } + SegmentBaseSingleSidxManifestWriterImpl dashManifestWriter = new SegmentBaseSingleSidxManifestWriterImpl( + trackFamilies, dashedFiles, + trackBitrate, trackFilename, + trackKeyIds); + MPDDocument mpdDocument = dashManifestWriter.getManifest(); + + XmlOptions xmlOptions = new XmlOptions(); + //xmlOptions.setUseDefaultNamespace(); + HashMap ns = new HashMap(); + //ns.put("urn:mpeg:DASH:schema:MPD:2011", ""); + ns.put("urn:mpeg:cenc:2013", "cenc"); + xmlOptions.setSaveSuggestedPrefixes(ns); + xmlOptions.setSaveAggressiveNamespaces(); + xmlOptions.setUseDefaultNamespace(); + xmlOptions.setSavePrettyPrint(); + + mpdDocument.save(new File(this.outputDirectory, "Manifest.mpd"), xmlOptions); + } + + @Override + Mp4Builder getFileBuilder(FragmentIntersectionFinder fragmentIntersectionFinder, Movie m) { + DashEncryptedBuilder dashBuilder = new DashEncryptedBuilder(); + for (Track track : m.getTracks()) { + dashBuilder.getKeyIds().put(track, UUIDConverter.convert(this.keyid)); + dashBuilder.getKeys().put(track, this.key); + } + dashBuilder.setIntersectionFinder(fragmentIntersectionFinder); + // dashBuilder.setPsshBoxes(Map inputFiles; + + @Option(name = "--outputdir", aliases = "-o", + usage = "output directory - if no output directory is given the " + + "current working directory is used.", + metaVar = "PATH") + protected File outputDirectory = new File(""); + + + @Override + public int run() throws IOException { + if (!(outputDirectory.exists() ^ outputDirectory.mkdirs())) { + System.err.println("Output directory does not exist and cannot be created."); + } + + long start = System.currentTimeMillis(); + + Map trackOriginalFilename = createTracks(); + + // generate filenames for later reference + Map trackFilename = generateFilenames(trackOriginalFilename); + + // export the dashed single track MP4s + Map mp4Files = writeSingleTrackMp4s(trackFilename); + + System.out.println("Finished write in " + (System.currentTimeMillis() - start) + "ms"); + return 0; + } + + Mp4Builder getFileBuilder(Movie m) { + return new DefaultMp4Builder(); + } + + private HashMap writeSingleTrackMp4s(Map filenames) throws IOException { + + HashMap containers = new HashMap(); + for (Track track : filenames.keySet()) { + String filename = filenames.get(track); + Movie movie = new Movie(); + movie.addTrack(track); + + System.out.print("Creating model for " + filename + "... "); + Mp4Builder mp4Builder = getFileBuilder(movie); + Container isoFile = mp4Builder.build(movie); + + System.out.print("Writing... "); + WritableByteChannel wbc = new FileOutputStream( + new File(outputDirectory, filename)).getChannel(); + try { + isoFile.writeContainer(wbc); + containers.put(track, isoFile); + } finally { + wbc.close(); + } + System.out.println("Done."); + } + return containers; + } + + /** + * Generates filenames from type, language and bitrate. + * + * @return a descriptive filename type[-lang]-bitrate.mp4 + */ + private Map generateFilenames(Map trackOriginalFilename) { + HashMap filenames = new HashMap(); + for (Track track : trackOriginalFilename.keySet()) { + String originalFilename = trackOriginalFilename.get(track); + originalFilename = originalFilename.replace(".aac", ""); + originalFilename = originalFilename.replace(".ec3", ""); + originalFilename = originalFilename.replace(".ac3", ""); + originalFilename = originalFilename.replace(".dtshd", ""); + filenames.put(track, String.format("%s.mp4", originalFilename)); + } + return filenames; + } + + /** + * Creates a Map with Track as key and originating filename as value. + * + * @return Track too originating file map + * @throws java.io.IOException + */ + Map createTracks() throws IOException { + HashMap track2File = new HashMap(); + for (File inputFile : inputFiles) { + if (inputFile.getName().endsWith(".aac")) { + Track track = new AACTrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".ac3")) { + Track track = new AC3TrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".ec3")) { + Track track = new EC3TrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else if (inputFile.getName().endsWith(".dtshd")) { + Track track = new DTSTrackImpl(new FileDataSourceImpl(inputFile)); + track2File.put(track, inputFile.getName()); + } else { + System.err.println("Cannot identify type of " + inputFile + ". Extensions aac, ac3, ec3 or dtshd are known."); + } + } + + return track2File; + } + +} \ No newline at end of file diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentBaseSingleSidxManifestWriterImpl.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentBaseSingleSidxManifestWriterImpl.java new file mode 100644 index 0000000..ea70280 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentBaseSingleSidxManifestWriterImpl.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014 castLabs GmbH, Berlin + */ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.authoring.Track; +import mpegDASHSchemaMPD2011.AdaptationSetType; +import mpegDASHSchemaMPD2011.PeriodType; +import mpegDASHSchemaMPD2011.RepresentationType; +import mpegDASHSchemaMPD2011.SegmentBaseType; +import org.apache.xmlbeans.GDuration; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.castlabs.dash.helpers.ManifestHelper.calculateIndexRange; +import static com.castlabs.dash.helpers.ManifestHelper.createRepresentation; + + +/** + * Creates a single SIDX manifest. + */ +public class SegmentBaseSingleSidxManifestWriterImpl extends AbstractManifestWriter { + + + public SegmentBaseSingleSidxManifestWriterImpl(Map> trackFamilies, Map trackContainer, Map trackBitrates, Map trackFilenames, Map trackKeyIds) { + super(trackFamilies, trackContainer, trackBitrates, trackFilenames, trackKeyIds); + } + + protected void createPeriod(PeriodType periodType) throws IOException { + + double maxDurationInSeconds = -1; + + for (String trackFamily : trackFamilies.keySet()) { + List tracks = trackFamilies.get(trackFamily); + + AdaptationSetType adaptationSet = createAdaptationSet(periodType, tracks); + + + for (Track track : tracks) { + RepresentationType representation = createRepresentation(adaptationSet, track); + SegmentBaseType segBaseType = representation.addNewSegmentBase(); + createInitialization(segBaseType.addNewInitialization(), track); + + segBaseType.setTimescale(track.getTrackMetaData().getTimescale()); + segBaseType.setIndexRangeExact(true); + segBaseType.setIndexRange(calculateIndexRange(trackContainer.get(track))); + + representation.setId(trackFilenames.get(track)); + representation.setStartWithSAP(1); + representation.setBandwidth(trackBitrates.get(track)); + representation.addNewBaseURL().setStringValue(trackFilenames.get(track)); + + + double durationInSeconds = (double) track.getDuration() / track.getTrackMetaData().getTimescale(); + maxDurationInSeconds = Math.max(maxDurationInSeconds, durationInSeconds); + } + + + } + + + periodType.setDuration(new GDuration( + 1, 0, 0, 0, (int) (maxDurationInSeconds / 3600), + (int) ((maxDurationInSeconds % 3600) / 60), + (int) (maxDurationInSeconds % 60), BigDecimal.ZERO)); + + + } + + +} diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentListManifestWriterImpl.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentListManifestWriterImpl.java new file mode 100644 index 0000000..9b3d3cc --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/dashfragmenter/mp4todash/SegmentListManifestWriterImpl.java @@ -0,0 +1,102 @@ +/* + * Copyright 2014 castLabs GmbH, Berlin + */ +package com.castlabs.dash.dashfragmenter.mp4todash; + +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.Container; +import com.coremedia.iso.boxes.fragment.MovieFragmentBox; +import com.coremedia.iso.boxes.fragment.TrackRunBox; +import com.googlecode.mp4parser.authoring.Track; +import mpegDASHSchemaMPD2011.*; +import org.apache.xmlbeans.GDuration; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.castlabs.dash.helpers.ManifestHelper.createRepresentation; + + +/** + * Creates a single SIDX manifest. + */ +public class SegmentListManifestWriterImpl extends AbstractManifestWriter { + + + public SegmentListManifestWriterImpl(Map> trackFamilies, Map trackContainer, Map trackBitrates, Map trackFilenames, Map trackKeyIds) { + super(trackFamilies, trackContainer, trackBitrates, trackFilenames, trackKeyIds); + } + + + protected void createPeriod(PeriodType periodType) throws IOException { + + double maxDurationInSeconds = -1; + + for (String trackFamily : trackFamilies.keySet()) { + List tracks = trackFamilies.get(trackFamily); + for (Track track : tracks) { + double durationInSeconds = (double) track.getDuration() / track.getTrackMetaData().getTimescale(); + maxDurationInSeconds = Math.max(maxDurationInSeconds, durationInSeconds); + } + AdaptationSetType adaptationSet = createAdaptationSet(periodType, tracks); + + + for (Track track : tracks) { + RepresentationType representation = createRepresentation(adaptationSet, track); + + representation.setStartWithSAP(1); + representation.setBandwidth(trackBitrates.get(track)); + representation.addNewBaseURL().setStringValue(trackFilenames.get(track)); + long offset = 0; + Iterator boxes = trackContainer.get(track).getBoxes().iterator(); + SegmentListType segmentList = representation.addNewSegmentList(); + SegmentTimelineType segmentTimeline = segmentList.addNewSegmentTimeline(); + createInitialization(segmentList.addNewInitialization(), track); + long time = 0; + while (boxes.hasNext()) { + Box b = boxes.next(); + if ("moof".equals(b.getType())) { + + SegmentTimelineType.S s = segmentTimeline.addNewS(); + MovieFragmentBox moof = (MovieFragmentBox) b; + assert moof.getTrackRunBoxes().size() == 1 : "Ouch - doesn't with mutiple trun"; + + TrackRunBox trun = moof.getTrackRunBoxes().get(0); + long segmentDuration = 0; + for (TrackRunBox.Entry entry : trun.getEntries()) { + segmentDuration += entry.getSampleDuration(); + } + s.setD(segmentDuration); + s.setT(time); + time += segmentDuration; + + SegmentURLType segmentURL = segmentList.addNewSegmentURL(); + Box mdat = boxes.next(); + segmentURL.setMediaRange( + String.format("%s-%s", + offset, offset + moof.getSize() + mdat.getSize())); + + offset += moof.getSize() + mdat.getSize(); + } else { + offset += b.getSize(); + } + } + + } + } + + //adaptationSetVid.setPar(); + + periodType.setDuration(new GDuration( + 1, 0, 0, 0, (int) (maxDurationInSeconds / 3600), + (int) ((maxDurationInSeconds % 3600) / 60), + (int) (maxDurationInSeconds % 60), BigDecimal.ZERO)); + + + } + +} diff --git a/dash.fragmencrypter/src/main/java/com/castlabs/dash/helpers/ManifestHelper.java b/dash.fragmencrypter/src/main/java/com/castlabs/dash/helpers/ManifestHelper.java new file mode 100644 index 0000000..4325d40 --- /dev/null +++ b/dash.fragmencrypter/src/main/java/com/castlabs/dash/helpers/ManifestHelper.java @@ -0,0 +1,503 @@ +package com.castlabs.dash.helpers; + +import com.coremedia.iso.Hex; +import com.coremedia.iso.boxes.Box; +import com.coremedia.iso.boxes.Container; +import com.coremedia.iso.boxes.OriginalFormatBox; +import com.coremedia.iso.boxes.SampleDescriptionBox; +import com.coremedia.iso.boxes.h264.AvcConfigurationBox; +import com.coremedia.iso.boxes.sampleentry.AbstractSampleEntry; +import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; +import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.boxes.AC3SpecificBox; +import com.googlecode.mp4parser.boxes.DTSSpecificBox; +import com.googlecode.mp4parser.boxes.EC3SpecificBox; +import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig; +import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor; +import com.googlecode.mp4parser.boxes.threegpp26244.SegmentIndexBox; +import com.googlecode.mp4parser.util.Path; +import mpegDASHSchemaMPD2011.AdaptationSetType; +import mpegDASHSchemaMPD2011.DescriptorType; +import mpegDASHSchemaMPD2011.RepresentationType; +import org.apache.commons.lang.math.Fraction; + +import java.io.IOException; +import java.util.List; + +/** + * Some conversion from Track representation to Manifest specifics shared by DASH manifests of all kinds. + */ +public class ManifestHelper { + + public static String convertFramerate(double vrate) { + String frameRate = null; + if ((vrate > 23) && (vrate < 24)) { + frameRate = "24000/1001"; + } else if (vrate == 24) { + frameRate = "24000/1000"; + } else if ((vrate > 24) && ((vrate < 25) || (vrate == 25))) { + frameRate = "25000/1000"; + } else if ((vrate > 29) && (vrate < 30)) { + frameRate = "30000/1001"; + } else if (vrate == 30) { + frameRate = "30000/1000"; + } else if (vrate == 50) { + frameRate = "50000/1000"; + } else if ((vrate > 59) && (vrate < 60)) { + frameRate = "60000/1001"; + } else if (vrate == 60) { + frameRate = "60000/1000"; + } else { + System.out.println("Framerate " + vrate + " is not supported"); + System.exit(1); + } + return frameRate; + } + + public static String calculateIndexRange(Container isoFile) throws IOException { + SegmentIndexBox sidx = (SegmentIndexBox) Path.getPath(isoFile, "/sidx"); + long start = 0; + for (Box box : isoFile.getBoxes()) { + if (box == sidx) { + break; + } else { + start += box.getSize(); + } + } + // long start = sidx.getOffset(); getOffset works for parsed files only + long end = sidx.getSize() + start; + return String.format("%s-%s", start, end); + } + + + /** + * Creates a representation and adjusts the AdaptionSet's attributes maxFrameRate, maxWidth, maxHeight. + * Also creates AudioChannelConfiguration. + */ + public static RepresentationType createRepresentation(AdaptationSetType adaptationSet, Track track) { + RepresentationType representation = adaptationSet.addNewRepresentation(); + if (track.getHandler().equals("vide")) { + + long videoHeight = (long) track.getTrackMetaData().getHeight(); + long videoWidth = (long) track.getTrackMetaData().getWidth(); + double framesPerSecond = (double) (track.getSamples().size() * track.getTrackMetaData().getTimescale()) / track.getDuration(); + + adaptationSet.setMaxFrameRate(convertFramerate( + Math.max(adaptationSet.isSetMaxFrameRate() ? Fraction.getFraction(adaptationSet.getMaxFrameRate()).doubleValue() : 0, + framesPerSecond))); + + adaptationSet.setMaxWidth(Math.max(adaptationSet.isSetMaxWidth() ? adaptationSet.getMaxWidth() : 0, + videoHeight)); + adaptationSet.setMaxHeight(Math.max(adaptationSet.isSetMaxHeight() ? adaptationSet.getMaxHeight() : 0, + videoWidth)); + + adaptationSet.setPar("1:1"); + // too hard to find it out. Ignoring even though it should be set according to DASH-AVC-264-v2.00-hd-mca.pdf + + representation.setMimeType("video/mp4"); + representation.setCodecs(getVideoCodecs(track)); + representation.setWidth(videoWidth); + representation.setHeight(videoHeight); + representation.setFrameRate(convertFramerate(framesPerSecond)); + representation.setSar("1:1"); + // too hard to find it out. Ignoring even though it should be set according to DASH-AVC-264-v2.00-hd-mca.pdf + } + + if (track.getHandler().equals("soun")) { + + AudioQuality audioQ = getAudioQuality(track); + + + representation.setMimeType("audio/mp4"); + representation.setCodecs(audioQ.fourCC); + representation.setAudioSamplingRate(String.valueOf(audioQ.samplingRate)); + + DescriptorType audio_channel_conf = representation.addNewAudioChannelConfiguration(); + audio_channel_conf.setSchemeIdUri(audioQ.audioChannelScheme); + audio_channel_conf.setValue(audioQ.audioChannelValue); + + } + return representation; + } + + private static AudioQuality getAudioQuality(Track track) { + String type = track.getSampleDescriptionBox().getSampleEntry().getType(); + if (type.equals("enca")) { + OriginalFormatBox frma = track.getSampleDescriptionBox().getSampleEntry().getBoxes(OriginalFormatBox.class, true).get(0); + type = frma.getDataFormat(); + } + if (type.equals("ac-3")) { + return getAc3AudioQuality(track); + } else if (type.equals("ec-3")) { + return getEc3AudioQuality(track); + } else if (type.startsWith("dts")) { + return getDtsAudioQuality(track); + } else if (type.equals("mp4a")) { + return getAacAudioQuality(track); + } else { + throw new RuntimeException("I don't know how to get AudioQuality for " + type); + } + } + + private static AudioQuality getDtsAudioQuality(Track track) { + AudioQuality l = new AudioQuality(); + + final AudioSampleEntry ase = getAudioSampleEntry(track, l); + l.fourCC = getFormat(ase); + + final DTSSpecificBox dtsSpecificBox = ase.getBoxes(DTSSpecificBox.class).get(0); + if (dtsSpecificBox == null) { + throw new RuntimeException("DTS track misses DTSSpecificBox!"); + } + l.bitrate = dtsSpecificBox.getAvgBitRate(); + l.samplingRate = dtsSpecificBox.getDTSSamplingFrequency(); + l.bitPerSample = dtsSpecificBox.getPcmSampleDepth(); + l.audioChannelValue = Integer.toString(getNumChannelsAndMask(dtsSpecificBox)[0]); + l.audioChannelScheme = "urn:dts:dash:audio_channel_configuration:2012"; + + return l; + } + + //todo: reuse smoothstreaming code, it's copied here + private static int[] getNumChannelsAndMask(DTSSpecificBox dtsSpecificBox) { + final int channelLayout = dtsSpecificBox.getChannelLayout(); + int numChannels = 0; + int dwChannelMask = 0; + if ((channelLayout & 0x0001) == 0x0001) { + //0001h Center in front of listener 1 + numChannels += 1; + dwChannelMask |= 0x00000004; //SPEAKER_FRONT_CENTER + } + if ((channelLayout & 0x0002) == 0x0002) { + //0002h Left/Right in front 2 + numChannels += 2; + dwChannelMask |= 0x00000001; //SPEAKER_FRONT_LEFT + dwChannelMask |= 0x00000002; //SPEAKER_FRONT_RIGHT + } + if ((channelLayout & 0x0004) == 0x0004) { + //0004h Left/Right surround on side in rear 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0008) == 0x0008) { + //0008h Low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x0010) == 0x0010) { + //0010h Center surround in rear 1 + numChannels += 1; + dwChannelMask |= 0x00000100; //SPEAKER_BACK_CENTER + } + if ((channelLayout & 0x0020) == 0x0020) { + //0020h Left/Right height in front 2 + numChannels += 2; + dwChannelMask |= 0x00001000; //SPEAKER_TOP_FRONT_LEFT + dwChannelMask |= 0x00004000; //SPEAKER_TOP_FRONT_RIGHT + } + if ((channelLayout & 0x0040) == 0x0040) { + //0040h Left/Right surround in rear 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x0080) == 0x0080) { + //0080h Center Height in front 1 + numChannels += 1; + dwChannelMask |= 0x00002000; //SPEAKER_TOP_FRONT_CENTER + } + if ((channelLayout & 0x0100) == 0x0100) { + //0100h Over the listener’s head 1 + numChannels += 1; + dwChannelMask |= 0x00000800; //SPEAKER_TOP_CENTER + } + if ((channelLayout & 0x0200) == 0x0200) { + //0200h Between left/right and center in front 2 + numChannels += 2; + dwChannelMask |= 0x00000040; //SPEAKER_FRONT_LEFT_OF_CENTER + dwChannelMask |= 0x00000080; //SPEAKER_FRONT_RIGHT_OF_CENTER + } + if ((channelLayout & 0x0400) == 0x0400) { + //0400h Left/Right on side in front 2 + numChannels += 2; + dwChannelMask |= 0x00000200; //SPEAKER_SIDE_LEFT + dwChannelMask |= 0x00000400; //SPEAKER_SIDE_RIGHT + } + if ((channelLayout & 0x0800) == 0x0800) { + //0800h Left/Right surround on side 2 + numChannels += 2; + //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x1000) == 0x1000) { + //1000h Second low frequency effects subwoofer 1 + numChannels += 1; + dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY + } + if ((channelLayout & 0x2000) == 0x2000) { + //2000h Left/Right height on side 2 + numChannels += 2; + dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT + dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT + } + if ((channelLayout & 0x4000) == 0x4000) { + //4000h Center height in rear 1 + numChannels += 1; + dwChannelMask |= 0x00010000; //SPEAKER_TOP_BACK_CENTER + } + if ((channelLayout & 0x8000) == 0x8000) { + //8000h Left/Right height in rear 2 + numChannels += 2; + dwChannelMask |= 0x00008000; //SPEAKER_TOP_BACK_LEFT + dwChannelMask |= 0x00020000; //SPEAKER_TOP_BACK_RIGHT + } + if ((channelLayout & 0x10000) == 0x10000) { + //10000h Center below in front + numChannels += 1; + } + if ((channelLayout & 0x20000) == 0x20000) { + //20000h Left/Right below in front + numChannels += 2; + } + return new int[]{numChannels, dwChannelMask}; + } + + private static AudioQuality getAacAudioQuality(Track track) { + AudioQuality l = new AudioQuality(); + AudioSampleEntry ase = (AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry(); + l.samplingRate = ase.getSampleRate(); + final ESDescriptorBox esDescriptorBox = ase.getBoxes(ESDescriptorBox.class).get(0); + final DecoderConfigDescriptor decoderConfigDescriptor = esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor(); + final AudioSpecificConfig audioSpecificConfig = decoderConfigDescriptor.getAudioSpecificInfo(); + if (audioSpecificConfig.getSbrPresentFlag() == 1) { + l.fourCC = "mp4a.40.5"; + } else if (audioSpecificConfig.getPsPresentFlag() == 1) { + l.fourCC = "mp4a.40.29"; + } else { + l.fourCC = "mp4a.40.2"; + } + l.bitrate = decoderConfigDescriptor.getAvgBitRate(); + l.samplingRate = ase.getSampleRate(); + l.audioChannelScheme = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"; + l.audioChannelValue = String.valueOf(audioSpecificConfig.getChannelConfiguration()); + l.bitPerSample = ase.getSampleSize(); + return l; + } + + private static AudioQuality getAc3AudioQuality(Track track) { + AudioQuality l = new AudioQuality(); + l.fourCC = "AC-3"; + + AudioSampleEntry ase = getAudioSampleEntry(track, l); + final AC3SpecificBox ac3SpecificBox = ase.getBoxes(AC3SpecificBox.class).get(0); + if (ac3SpecificBox == null) { + throw new RuntimeException("AC-3 track misses AC3SpecificBox!"); + } + + int bitRateCode = ac3SpecificBox.getBitRateCode(); + /* + Table A.2: Bit_rate_code table +bit_rate_code Exact bit rate (kbit/s) bit_rate_code Bit rate upper limit (kbit/s) +"000000" (0) 32 "100000" (32) 32 +"000001" (1) 40 "100001" (33) 40 +"000010" (2) 48 "100010" (34) 48 +"000011" (3) 56 "100011" (35) 56 +"000100" (4) 64 "100100" (36) 64 +"000101" (5) 80 "100101" (37) 80 +"000110" (6) 96 "100110" (38) 96 +"000111" (7) 112 "100111" (39) 112 +"001000" (8) 128 "101000" (40) 128 +"001001" (9) 160 "101001" (41) 160 +"001010" (10) 192 "101010" (42) 192 +"001011" (11) 224 "101011" (43) 224 +"001100" (12) 256 "101100" (44) 256 +"001101" (13) 320 "101101" (45) 320 +"001110" (14) 384 "101110" (46) 384 +"001111" (15) 448 "101111" (47) 448 +"010000" (16) 512 "110000" (48) 512 +"010001" (17) 576 "110001" (49) 576 +"010010" (18) 640 "110010" (50) 640 + */ + //remove upper limit indicator + bitRateCode = bitRateCode << 1; + switch (bitRateCode) { + case 0: + l.bitrate = 32; + break; + case 1: + l.bitrate = 40; + break; + case 2: + l.bitrate = 48; + break; + case 3: + l.bitrate = 56; + break; + case 4: + l.bitrate = 64; + break; + case 5: + l.bitrate = 80; + break; + case 6: + l.bitrate = 96; + break; + case 7: + l.bitrate = 112; + break; + case 8: + l.bitrate = 128; + break; + case 9: + l.bitrate = 160; + break; + case 10: + l.bitrate = 192; + break; + case 11: + l.bitrate = 224; + break; + case 12: + l.bitrate = 256; + break; + case 13: + l.bitrate = 320; + break; + case 14: + l.bitrate = 384; + break; + case 15: + l.bitrate = 448; + break; + case 16: + l.bitrate = 512; + break; + case 17: + l.bitrate = 576; + break; + case 18: + l.bitrate = 640; + break; + } + l.bitrate *= 1024; //bit per sec + + int audioChannelValue = getDolbyAudioChannelValue(ac3SpecificBox.getAcmod(), ac3SpecificBox.getLfeon()); + l.audioChannelValue = Hex.encodeHex(new byte[]{(byte) ((audioChannelValue >> 8) & 0xFF), (byte) (audioChannelValue & 0xFF)}); + l.audioChannelScheme = "urn:dolby:dash:audio_channel_configuration:2011"; + + return l; + } + + private static AudioQuality getEc3AudioQuality(Track track) { + AudioQuality l = new AudioQuality(); + l.fourCC = "EC-3"; + + + AudioSampleEntry ase = getAudioSampleEntry(track, l); + final EC3SpecificBox ec3SpecificBox = ase.getBoxes(EC3SpecificBox.class).get(0); + if (ec3SpecificBox == null) { + throw new RuntimeException("EC-3 track misses EC3SpecificBox!"); + } + l.bitrate = ec3SpecificBox.getDataRate() * 1024; + + final List ec3SpecificBoxEntries = ec3SpecificBox.getEntries(); + int audioChannelValue = 0; + for (EC3SpecificBox.Entry ec3SpecificBoxEntry : ec3SpecificBoxEntries) { + audioChannelValue |= getDolbyAudioChannelValue(ec3SpecificBoxEntry.acmod, ec3SpecificBoxEntry.lfeon); + } + l.audioChannelValue = Hex.encodeHex(new byte[]{(byte) ((audioChannelValue >> 8) & 0xFF), (byte) (audioChannelValue & 0xFF)}); + l.audioChannelScheme = "urn:dolby:dash:audio_channel_configuration:2011"; + return l; + } + + private static AudioSampleEntry getAudioSampleEntry(Track track, AudioQuality l) { + final AudioSampleEntry ase = (AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry(); + l.samplingRate = ase.getSampleRate(); + + return ase; + } + + private static int getDolbyAudioChannelValue(int acmod, int lfeon) { + int audioChannelValue; + switch (acmod) { + case 0: + audioChannelValue = 0xA000; + break; + case 1: + audioChannelValue = 0x4000; + break; + case 2: + audioChannelValue = 0xA000; + break; + case 3: + audioChannelValue = 0xE000; + break; + case 4: + audioChannelValue = 0xA100; + break; + case 5: + audioChannelValue = 0xE100; + break; + case 6: + audioChannelValue = 0xB800; + break; + case 7: + audioChannelValue = 0xF800; + break; + default: + throw new RuntimeException("Unexpected acmod " + acmod); + } + if (lfeon == 1) { + audioChannelValue += 1; + } + return audioChannelValue; + } + + private static String getVideoCodecs(Track track) { + + SampleDescriptionBox stsd = track.getSampleDescriptionBox(); + VisualSampleEntry vse = (VisualSampleEntry) stsd.getSampleEntry(); + String type = vse.getType(); + if (type.equals("encv")) { + OriginalFormatBox frma = vse.getBoxes(OriginalFormatBox.class, true).get(0); + type = frma.getDataFormat(); + } + + if ("avc1".equals(type)) { + AvcConfigurationBox avcConfigurationBox = vse.getBoxes(AvcConfigurationBox.class).get(0); + List spsbytes = avcConfigurationBox.getSequenceParameterSets(); + byte[] CodecInit = new byte[3]; + CodecInit[0] = spsbytes.get(0)[1]; + CodecInit[1] = spsbytes.get(0)[2]; + CodecInit[2] = spsbytes.get(0)[3]; + return (type + "." + Hex.encodeHex(CodecInit)).toLowerCase(); + } else { + throw new InternalError("I don't know how to handle video of type " + vse.getType()); + } + + } + + protected static String getFormat(AbstractSampleEntry se) { + String type = se.getType(); + if (type.equals("encv") || type.equals("enca") || type.equals("encv")) { + OriginalFormatBox frma = se.getBoxes(OriginalFormatBox.class, true).get(0); + type = frma.getDataFormat(); + } + return type; + } + + private static class AudioQuality { + public String fourCC; + public long bitrate; + public long samplingRate; + public String audioChannelScheme; + public String audioChannelValue; + + public int bitPerSample; + } +} diff --git a/dash.fragmencrypter/src/main/resources/jarname.txt b/dash.fragmencrypter/src/main/resources/jarname.txt new file mode 100644 index 0000000..e67b789 --- /dev/null +++ b/dash.fragmencrypter/src/main/resources/jarname.txt @@ -0,0 +1 @@ +${project.build.finalName}.${project.packaging} \ No newline at end of file diff --git a/dash.fragmencrypter/src/main/resources/tool.txt b/dash.fragmencrypter/src/main/resources/tool.txt new file mode 100644 index 0000000..387fa85 --- /dev/null +++ b/dash.fragmencrypter/src/main/resources/tool.txt @@ -0,0 +1 @@ +${project.groupId} ${project.artifactId} ${project.version} \ No newline at end of file diff --git a/dash.xsd/pom.xml b/dash.xsd/pom.xml new file mode 100644 index 0000000..d489cb1 --- /dev/null +++ b/dash.xsd/pom.xml @@ -0,0 +1,50 @@ + + + + com.castlabs.dash + dash.encrypt + 1.0-SNAPSHOT + + 4.0.0 + + dash.xsd + + + + + src/main/resources + true + + + install + + + + org.codehaus.mojo + xmlbeans-maven-plugin + 2.3.3 + + + + xmlbeans + + + + + src/main/resources/xsd/DASH-MPD.xsd + false + src/main/resources/xsd + + + + + + + + org.apache.xmlbeans + xmlbeans + 2.6.0 + + + \ No newline at end of file diff --git a/dash.xsd/src/main/resources/xsd/DASH-MPD.xsd b/dash.xsd/src/main/resources/xsd/DASH-MPD.xsd new file mode 100644 index 0000000..f94f451 --- /dev/null +++ b/dash.xsd/src/main/resources/xsd/DASH-MPD.xsd @@ -0,0 +1,393 @@ + + + + + + + + Media Presentation Description + + This Schema defines the Media Presentation Description for MPEG-DASH. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dash.xsd/src/main/resources/xsd/cenc.xsd b/dash.xsd/src/main/resources/xsd/cenc.xsd new file mode 100644 index 0000000..7ed360b --- /dev/null +++ b/dash.xsd/src/main/resources/xsd/cenc.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dash.xsd/src/main/resources/xsd/xlink.xsd b/dash.xsd/src/main/resources/xsd/xlink.xsd new file mode 100644 index 0000000..76b67be --- /dev/null +++ b/dash.xsd/src/main/resources/xsd/xlink.xsd @@ -0,0 +1,271 @@ + + + + + This schema document provides attribute declarations and + attribute group, complex type and simple type definitions which can be used in + the construction of user schemas to define the structure of particular linking + constructs, e.g. + + + + + + + ... + + ... + + + ... +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Intended for use as the type of user-declared elements to make them + simple links. + + + + + + + + + + + + + + + + + + + + + + + + + Intended for use as the type of user-declared elements to make them + extended links. + Note that the elements referenced in the content model are all abstract. + The intention is that by simply declaring elements with these as their + substitutionGroup, all the right things will happen. + + + + + + + + + + + + + + xml:lang is not required, but provides much of the + motivation for title elements in addition to attributes, and so + is provided here for convenience. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + label is not required, but locators have no particular + XLink function if they are not labeled. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + from and to have default behavior when values are missing + + + + + + + + + + + + + + + + + diff --git a/dash.xsd/src/main/resources/xsd/xml.xsd b/dash.xsd/src/main/resources/xsd/xml.xsd new file mode 100644 index 0000000..0f7db9d --- /dev/null +++ b/dash.xsd/src/main/resources/xsd/xml.xsd @@ -0,0 +1,327 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + + http://www.w3.org/XML/1998/namespace.html + + and + + http://www.w3.org/TR/REC-xml + + for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about + how to refer to this schema document from your own + XSD schema documents + + and aboutthe + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. +

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. +

+ +
+
+
+ + + + + + +
+ + + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. +

+ +

+ See + http://www.w3.org/TR/xmlbase/ + + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification. +

+ +

+ See + http://www.w3.org/TR/xml-id/ + + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

+ About this schema document +

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allowxml:base, + xml:lang, + xml:space + or + xml:id + attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+                        <schema . . .>
+                        . . .
+                        <import namespace="http://www.w3.org/XML/1998/namespace"
+                        schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+                    
+

+ or +

+
+                        <import namespace="http://www.w3.org/XML/1998/namespace"
+                        schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+                    
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+                        <type . . .>
+                        . . .
+                        <attributeGroup ref="xml:specialAttrs"/>
+                    
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

+ Versioning policy for this schema document +

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..045def9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.castlabs.dash + dash.encrypt + pom + 1.0-SNAPSHOT + + dash.xsd + dash.fragmencrypter + + + + scm:git:git@github.com:castlabs/dashencrypt.git + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.1 + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + + + + + + releases + https://repository.castlabs.com/content/repositories/releases + + + snapshots + https://repository.castlabs.com/content/repositories/snapshots + + + +