com.castlabs.csf.cff.CreateStreamingDeliveryTargetFileset.java Source code

Java tutorial

Introduction

Here is the source code for com.castlabs.csf.cff.CreateStreamingDeliveryTargetFileset.java

Source

/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.castlabs.csf.cff;

import com.castlabs.csf.AbstractCommand;
import com.coremedia.iso.Hex;
import com.coremedia.iso.boxes.Container;
import com.coremedia.iso.boxes.OriginalFormatBox;
import com.coremedia.iso.boxes.sampleentry.AbstractSampleEntry;
import com.googlecode.mp4parser.FileDataSourceImpl;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.FragmentIntersectionFinder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.*;
import org.apache.commons.io.FilenameUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.FileOptionHandler;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;

public class CreateStreamingDeliveryTargetFileset extends AbstractCommand {
    Logger logger;

    private static final String DEFAULT_LANG = "eng";

    @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<File> inputFiles;

    @Option(name = "--codec", aliases = { "-c" })
    String codec;

    @Option(name = "--language", aliases = { "-l" })
    String language;

    @Option(name = "--track-id", aliases = { "-t" })
    long trackId = -1;

    protected UUID keyid;
    protected SecretKey cek;

    @Option(name = "--uuid", aliases = "-u", usage = "UUID (KeyID)", depends = { "--content-encryption-key" })
    protected String encKid = null;

    @Option(name = "--content-encryption-key", aliases = "-k", usage = "Content Encryption Key", depends = {
            "--uuid" }

    )
    protected String encKeySecretKey = null;

    @Override
    public int run() throws Exception {
        logger = setupLogger();
        if (encKid != null) {
            this.keyid = UUID.fromString(this.encKid);
            this.cek = new SecretKeySpec(Hex.decodeHex(this.encKeySecretKey), "AES");
        }

        Map<Track, String> trackOriginalFilename = setupTracks();
        FragmentIntersectionFinder intersectionFinder = getFragmentStartSamples(trackOriginalFilename);
        Map<Track, String> filenames = generateFilenames(trackOriginalFilename);

        StreamingDeliveryTargetMp4Builder mp4Builder = new StreamingDeliveryTargetMp4Builder();
        mp4Builder.setIntersectionFinder(intersectionFinder);

        Movie m = new Movie();
        for (Map.Entry<Track, String> e : trackOriginalFilename.entrySet()) {
            if (keyid != null) {
                m.setTracks(Collections.<Track>singletonList(new CencEncryptingTrackImpl(e.getKey(), keyid, cek)));
            } else {
                m.setTracks(Collections.<Track>singletonList(e.getKey()));
            }
            String apid = "urn:dece:apid:org:castlabs:" + FilenameUtils.getBaseName(e.getValue());
            mp4Builder.setApid(apid);
            Container c = mp4Builder.build(m);
            String filename = filenames.get(e.getKey());
            FileOutputStream fos = new FileOutputStream(filename);
            logger.info(String.format("Writing %s (track_ID=%d, apid=%s)", filename,
                    e.getKey().getTrackMetaData().getTrackId(), apid));
            c.writeContainer(fos.getChannel());
            fos.close();
        }
        return 0;
    }

    private FragmentIntersectionFinder getFragmentStartSamples(Map<Track, String> trackOriginalFilename)
            throws CommandAbortException {

        Set<Long> syncSamples = null;
        int numSamples = -1;
        for (Track track : trackOriginalFilename.keySet()) {
            if (numSamples < 0) {
                numSamples = track.getSamples().size();
            }
            if (numSamples != track.getSamples().size()) {
                throw new CommandAbortException("All Tracks need the same number of samples");
            }

        }

        for (Track track : trackOriginalFilename.keySet()) {
            if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
                if (syncSamples == null) {
                    syncSamples = new HashSet<Long>();
                    for (long l : track.getSyncSamples()) {
                        syncSamples.add(l);
                    }
                } else {
                    Set<Long> syncSamples2 = new HashSet<Long>();
                    for (long l : track.getSyncSamples()) {
                        syncSamples2.add(l);
                    }
                    syncSamples.retainAll(syncSamples2);
                }
            }
        }
        if (syncSamples != null) {

            List<Long> syncSampleList = new ArrayList<Long>();
            syncSampleList.addAll(syncSamples);
            Collections.sort(syncSampleList);

            final long[] fragmentStartSamples = new long[syncSamples.size()];
            for (int i = 0; i < fragmentStartSamples.length; i++) {
                fragmentStartSamples[i] = syncSampleList.get(i);
            }

            return new FragmentIntersectionFinder() {
                @Override
                public long[] sampleNumbers(Track track) {
                    return fragmentStartSamples;
                }
            };
        } else {
            // they have all the same amount of samples ... easy
            Track t = trackOriginalFilename.keySet().iterator().next();
            double durationInSeconds = t.getDuration() / t.getTrackMetaData().getTimescale();
            int numberOfSamples = t.getSamples().size();
            int numberOfSamplesPer5Seconds = (int) Math.ceil(numberOfSamples / durationInSeconds * 5);
            final long[] fragmentStartSamples = new long[(int) Math
                    .ceil(numberOfSamples / numberOfSamplesPer5Seconds) + 1];
            for (int i = 0; i < fragmentStartSamples.length; i++) {
                fragmentStartSamples[i] = i * numberOfSamplesPer5Seconds + 1;
            }

            return new FragmentIntersectionFinder() {
                @Override
                public long[] sampleNumbers(Track track) {
                    return fragmentStartSamples;
                }
            };
        }
    }

    public Map<Track, String> setupTracks() throws IOException, CommandAbortException, XPathExpressionException,
            SAXException, ParserConfigurationException {
        Map<Track, String> track2File = new HashMap<Track, String>();
        List<File> xmls = new ArrayList<File>();
        for (File inputFile : inputFiles) {
            if (inputFile.getName().endsWith("mp4")) {
                Movie movie = MovieCreator.build(new FileDataSourceImpl(inputFile));
                for (Track track : movie.getTracks()) {
                    if (checkCodecAndLanguage(track, inputFile.getName())) {
                        track2File.put(track, inputFile.getName());
                    }
                }
            } else {
                Track track = null;
                if (inputFile.getName().endsWith(".aac")) {
                    track = new AACTrackImpl(new FileDataSourceImpl(inputFile));
                    logger.fine("Created AAC Track from " + inputFile.getName());
                } else if (inputFile.getName().endsWith(".h264")) {
                    track = new H264TrackImpl(new FileDataSourceImpl(inputFile));
                    logger.fine("Created H264 Track from " + inputFile.getName());
                } else if (inputFile.getName().endsWith(".ac3")) {
                    track = new AC3TrackImpl(new FileDataSourceImpl(inputFile));
                    logger.fine("Created AC3 Track from " + inputFile.getName());
                } else if (inputFile.getName().endsWith(".ec3")) {
                    track = new EC3TrackImpl(new FileDataSourceImpl(inputFile));
                    logger.fine("Created EC3 Track from " + inputFile.getName());
                } else if (inputFile.getName().endsWith(".dtshd")) {
                    track = new DTSTrackImpl(new FileDataSourceImpl(inputFile));
                    logger.fine("Created DTS HD Track from " + inputFile.getName());
                } else if (inputFile.getName().endsWith(".xml")) {
                    xmls.add(inputFile);

                } else {
                    logger.warning("Cannot identify type of " + inputFile
                            + ". Extensions mp4, aac, ac3, ec3 or dtshd are known.");
                }
                if (track != null) {
                    if (language != null) {
                        track.getTrackMetaData().setLanguage(language);
                    } else {
                        logger.fine("No language given for raw track - defaulting to " + DEFAULT_LANG);
                        track.getTrackMetaData().setLanguage(DEFAULT_LANG);
                    }
                    if ((checkCodecAndLanguage(track, inputFile.getName()))) {
                        track2File.put(track, inputFile.getName());
                    }
                }
            }
        }

        if (!xmls.isEmpty()) {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            dbFactory.setNamespaceAware(true);
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            HashMap<String, List<File>> languageGrouped = new HashMap<String, List<File>>();
            final HashMap<File, Long> startTimes = new HashMap<File, Long>();
            for (File xml : xmls) {
                Document doc = dBuilder.parse(xml);
                String lang = SMPTETTTrackImpl.getLanguage(doc);
                List<File> sameLangFiles = languageGrouped.get(lang);
                if (sameLangFiles == null) {
                    sameLangFiles = new ArrayList<File>();
                    languageGrouped.put(lang, sameLangFiles);
                }
                sameLangFiles.add(xml);
                startTimes.put(xml, SMPTETTTrackImpl.earliestTimestamp(doc));
            }

            for (Map.Entry<String, List<File>> stringListEntry : languageGrouped.entrySet()) {
                String lang = stringListEntry.getKey();
                List<File> sameLangFiles = stringListEntry.getValue();
                Collections.sort(sameLangFiles, new Comparator<File>() {
                    @Override
                    public int compare(File o1, File o2) {
                        return (int) (startTimes.get(o1) - startTimes.get(o2));
                    }
                });
                track2File.put(new SMPTETTTrackImpl(sameLangFiles.toArray(new File[sameLangFiles.size()])),
                        sameLangFiles.get(0).getName());
                logger.fine("Created SMPTE-TT Track from " + sameLangFiles + " in " + lang);
            }
        }
        return track2File;
    }

    private boolean checkCodecAndLanguage(Track track, String origFile) throws CommandAbortException {
        AbstractSampleEntry sampleEntry = track.getSampleDescriptionBox().getSampleEntry();
        if (codec == null) {
            if (trackId == -1) {
                if (track.getHandler().equals("vide")) {
                    if (track.getTrackMetaData().getTrackId() >= 1 && track.getTrackMetaData().getTrackId() <= 49) {
                        trackId = track.getTrackMetaData().getTrackId();
                    } else {
                        trackId = 1;
                    }
                } else if (track.getHandler().equals("soun")) {
                    if (track.getTrackMetaData().getTrackId() >= 100
                            && track.getTrackMetaData().getTrackId() <= 999) {
                        trackId = track.getTrackMetaData().getTrackId();
                    } else {
                        String codec = track.getSampleDescriptionBox().getSampleEntry().getType();
                        if ("mp4a".equals(codec)) {
                            trackId = 100;
                        } else if ("mlpa".equals(codec)) {
                            trackId = 200;
                        } else if ("dtsl".equals(codec)) {
                            trackId = 300;
                        } else if ("dtsh".equals(codec)) {
                            trackId = 300;
                        } else if ("dtsl".equals(codec)) {
                            trackId = 400;
                        } else if ("dtse".equals(codec)) {
                            trackId = 500;
                        } else if ("ac-3".equals(codec)) {
                            trackId = 600;
                        } else if ("ec-3".equals(codec)) {
                            trackId = 700;
                        } else {
                            throw new RuntimeException("Don't know which track number to assign");
                        }
                        trackId += track.getTrackMetaData().getLanguage().hashCode() % 100;
                        // this is not bullet-proof but should make that each codec/language combo gets its own
                        // track id without collisions.
                    }
                } else if (track.getHandler().equals("subt")) {
                    if (track.getTrackMetaData().getTrackId() >= 10000
                            && track.getTrackMetaData().getTrackId() <= 10999) {
                        trackId = track.getTrackMetaData().getTrackId();
                    } else {
                        trackId = 10000;
                    }
                } else {
                    throw new CommandAbortException(
                            "Don't know which trackId to assign for handler=" + track.getHandler());
                }
            }
            codec = getFormat(sampleEntry); // if not set in cmd line first track encountered sets the format for this adaptation set
        }
        track.getTrackMetaData().setTrackId(trackId);
        if (language == null) {
            language = track.getTrackMetaData().getLanguage();
        }
        if (!codec.equals(getFormat(sampleEntry))) {
            logger.warning("Skipping " + getFormat(track.getSampleDescriptionBox().getSampleEntry())
                    + " track extracted from " + origFile
                    + " as it is not the same codec as previously processed tracks");
            return false;
        }
        if (!language.equals(track.getTrackMetaData().getLanguage())) {
            logger.warning("Skipping " + getFormat(track.getSampleDescriptionBox().getSampleEntry())
                    + " track extracted from " + origFile
                    + " as it is not the same languae as previously processed tracks");
            return false;
        }
        return true;
    }

    protected String getFormat(AbstractSampleEntry se) {
        String type = se.getType();
        if (type.startsWith("enc")) {
            OriginalFormatBox frma = se.getBoxes(OriginalFormatBox.class, true).get(0);
            type = frma.getDataFormat();
        }
        return type;
    }

    private Map<Track, String> generateFilenames(Map<Track, String> trackOriginalFilename) {
        HashMap<Track, String> filenames = new HashMap<Track, String>();
        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", "");
            originalFilename = originalFilename.replace(".xml", "");
            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();
                }
            }
            if (track.getHandler().equals("soun")) {
                filenames.put(track, String.format("%s.uva", originalFilename));
            } else if (track.getHandler().equals("vide")) {
                filenames.put(track, String.format("%s.uvv", originalFilename));
            } else if (track.getHandler().equals("subt")) {
                filenames.put(track, String.format("%s.uvt", originalFilename));
            }

        }
        return filenames;
    }

}