All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jcodec.containers.mxf.MXFDemuxer Maven / Gradle / Ivy

There is a newer version: 0.2.5
Show newest version
package org.jcodec.containers.mxf;

import static java.util.Collections.unmodifiableList;
import static org.jcodec.containers.mxf.MXFConst.klMetadata;
import static org.jcodec.containers.mxf.model.MXFUtil.findAllMeta;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.jcodec.api.NotSupportedException;
import org.jcodec.common.DemuxerTrackMeta;
import org.jcodec.common.SeekableDemuxerTrack;
import org.jcodec.common.TrackType;
import org.jcodec.common.io.FileChannelWrapper;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.logging.Logger;
import org.jcodec.common.model.ColorSpace;
import org.jcodec.common.model.Packet;
import org.jcodec.common.model.Size;
import org.jcodec.common.model.TapeTimecode;
import org.jcodec.common.model.Packet.FrameType;
import org.jcodec.common.model.Rational;
import org.jcodec.containers.mxf.model.FileDescriptor;
import org.jcodec.containers.mxf.model.GenericDescriptor;
import org.jcodec.containers.mxf.model.GenericPictureEssenceDescriptor;
import org.jcodec.containers.mxf.model.GenericSoundEssenceDescriptor;
import org.jcodec.containers.mxf.model.IndexSegment;
import org.jcodec.containers.mxf.model.KLV;
import org.jcodec.containers.mxf.model.MXFMetadata;
import org.jcodec.containers.mxf.model.MXFPartition;
import org.jcodec.containers.mxf.model.MXFUtil;
import org.jcodec.containers.mxf.model.SourceClip;
import org.jcodec.containers.mxf.model.TimecodeComponent;
import org.jcodec.containers.mxf.model.TimelineTrack;
import org.jcodec.containers.mxf.model.UL;
import org.jcodec.containers.mxf.model.WaveAudioDescriptor;

/**
 * This class is part of JCodec ( www.jcodec.org ) This software is distributed
 * under FreeBSD License
 * 
 * MXF demuxer
 * 
 * @author The JCodec project
 * 
 */
public class MXFDemuxer {

    protected List metadata;
    protected MXFPartition header;
    protected List partitions;
    protected List indexSegments;
    protected SeekableByteChannel ch;
    protected MXFDemuxerTrack[] tracks;
    protected int totalFrames;
    protected double duration;
    protected TimecodeComponent timecode;

    public MXFDemuxer(SeekableByteChannel ch) throws IOException {
        this.ch = ch;
        ch.setPosition(0);
        parseHeader(ch);
        findIndex();
        tracks = findTracks();
        timecode = MXFUtil.findMeta(metadata, TimecodeComponent.class);
    }

    public static final class OP {
        public final static OP OP1a = new OP(1, 1);
        public final static OP OP1b = new OP(1, 2);
        public final static OP OP1c = new OP(1, 3);
        public final static OP OP2a = new OP(2, 1);
        public final static OP OP2b = new OP(2, 2);
        public final static OP OP2c = new OP(2, 3);
        public final static OP OP3a = new OP(3, 1);
        public final static OP OP3b = new OP(3, 2);
        public final static OP OP3c = new OP(3, 3);
        public final static OP OPAtom = new OP(0x10, 0);

        private final static OP[] _values = new OP[] { OP1a, OP1b, OP1c, OP2a, OP2b, OP2c, OP3a, OP3b, OP3c, OPAtom };
        public int major;
        public int minor;

        private OP(int major, int minor) {
            this.major = major;
            this.minor = minor;
        }

        public static OP[] values() {
            return _values;
        }
    }

    public OP getOp() {
        UL op = header.getPack().getOp();

        OP[] values = OP.values();
        for (int i = 0; i < values.length; i++) {
            OP op2 = values[i];
            if (op.get(12) == op2.major && op.get(13) == op2.minor)
                return op2;
        }
        return OP.OPAtom;
    }

    public MXFDemuxerTrack[] findTracks() throws IOException {
        List _tracks = findAllMeta(metadata, TimelineTrack.class);
        List descriptors = findAllMeta(metadata, FileDescriptor.class);
        Map tracks = new LinkedHashMap();
        for (TimelineTrack track : _tracks) {
            //SMPTE S377-1-2009
            //A Track ID value of zero (0) is deprecated, and shall not be used by MXF encoders conforming to this version of the MXF file format specification. 
            if (track.getTrackId() == 0 || track.getTrackNumber() == 0) {
                Logger.warn("trackId == 0 || trackNumber == 0");
                continue;
            }
            int trackId = track.getTrackId();
            if (tracks.containsKey(trackId)) {
                Logger.warn("duplicate trackId " + trackId);
                continue;
            }

            FileDescriptor descriptor = findDescriptor(descriptors, track.getTrackId());
            if (descriptor == null) {
                Logger.warn("No generic descriptor for track: " + track.getTrackId());
                if (descriptors.size() == 1 && descriptors.get(0).getLinkedTrackId() == 0) {
                    descriptor = descriptors.get(0);
                }
            }
            if (descriptor == null) {
                Logger.warn("Track without descriptor: " + track.getTrackId());
                continue;
            }
            int trackNumber = track.getTrackNumber();
            UL ul = UL.newUL(0x06, 0x0e, 0x2b, 0x34, 0x01, 0x02, 0x01, 0x01, 0x0d, 0x01, 0x03, 0x01,
                    (trackNumber >>> 24) & 0xff, (trackNumber >>> 16) & 0xff, (trackNumber >>> 8) & 0xff,
                    trackNumber & 0xff);
            MXFDemuxerTrack dt = createTrack(ul, track, descriptor);
            if (dt.getCodec() != null || (descriptor instanceof WaveAudioDescriptor)) {
                tracks.put(trackId, dt);
            }
        }

        return tracks.values().toArray(new MXFDemuxerTrack[tracks.size()]);
    }

    public static FileDescriptor findDescriptor(List descriptors, int trackId) {
        for (FileDescriptor descriptor : descriptors) {
            if (descriptor.getLinkedTrackId() == trackId) {
                return descriptor;
            }
        }
        return null;
    }

    protected MXFDemuxerTrack createTrack(UL ul, TimelineTrack track, GenericDescriptor descriptor) throws IOException {
        return new MXFDemuxerTrack(this, ul, track, descriptor);
    }

    public List getIndexes() {
        return indexSegments;
    }

    public List getEssencePartitions() {
        return partitions;
    }

    public TimecodeComponent getTimecode() {
        return timecode;
    }

    public void parseHeader(SeekableByteChannel ff) throws IOException {
        KLV kl;
        header = readHeaderPartition(ff);

        metadata = new ArrayList();

        partitions = new ArrayList();
        long nextPartition = ff.size();
        ff.setPosition(header.getPack().getFooterPartition());
        do {
            long thisPartition = ff.position();
            kl = KLV.readKL(ff);
            ByteBuffer fetchFrom = NIOUtils.fetchFromChannel(ff, (int) kl.len);
            header = MXFPartition.read(kl.key, fetchFrom, ff.position() - kl.offset, nextPartition);

            if (header.getPack().getNbEssenceContainers() > 0)
                partitions.add(0, header);

            metadata.addAll(0, readPartitionMeta(ff, header));

            ff.setPosition(header.getPack().getPrevPartition());
            nextPartition = thisPartition;
        } while (header.getPack().getThisPartition() != 0);
    }

    public static List readPartitionMeta(SeekableByteChannel ff, MXFPartition header) throws IOException {
        KLV kl;
        long basePos = ff.position();
        List local = new ArrayList();
        ByteBuffer metaBuffer = NIOUtils.fetchFromChannel(ff, (int) Math.max(0, header.getEssenceFilePos() - basePos));
        while (metaBuffer.hasRemaining() && (kl = KLV.readKLFromBuffer(metaBuffer, basePos)) != null) {
            if (metaBuffer.remaining() >= kl.len) {
                MXFMetadata meta = parseMeta(kl.key, NIOUtils.read(metaBuffer, (int) kl.len));
                if (meta != null)
                    local.add(meta);
            } else {
                break;
            }
        }
        return local;
    }

    public static MXFPartition readHeaderPartition(SeekableByteChannel ff) throws IOException {
        KLV kl;
        MXFPartition header = null;
        while ((kl = KLV.readKL(ff)) != null) {
            if (MXFConst.HEADER_PARTITION_KLV.equals(kl.key)) {
                ByteBuffer data = NIOUtils.fetchFromChannel(ff, (int) kl.len);
                header = MXFPartition.read(kl.key, data, ff.position() - kl.offset, 0);
                break;
            } else {
                ff.setPosition(ff.position() + kl.len);
            }
        }
        return header;
    }

    private static MXFMetadata parseMeta(UL ul, ByteBuffer _bb) {
        Class class1 = klMetadata.get(ul);
        if (class1 == null) {
            Logger.warn("Unknown metadata piece: " + ul);
            return null;
        }
        try {
            MXFMetadata meta = class1.getConstructor(UL.class).newInstance(ul);
            meta.readBuf(_bb);
            return meta;
        } catch (Exception e) {
        }
        Logger.warn("Unknown metadata piece: " + ul);
        return null;
    }

    private void findIndex() {
        indexSegments = new ArrayList();
        for (MXFMetadata meta : metadata) {
            if (meta instanceof IndexSegment) {
                IndexSegment is = (IndexSegment) meta;
                indexSegments.add(is);
                totalFrames += is.getIndexDuration();
                duration += ((double) is.getIndexEditRateDen() * is.getIndexDuration()) / is.getIndexEditRateNum();
            }
        }
    }

    public MXFDemuxerTrack[] getTracks() {
        return tracks;
    }

    public MXFDemuxerTrack getVideoTrack() {
        for (MXFDemuxerTrack track : tracks) {
            if (track.isVideo())
                return track;
        }
        return null;
    }

    public MXFDemuxerTrack[] getAudioTracks() {
        List audio = new ArrayList();
        for (MXFDemuxerTrack track : tracks) {
            if (track.isAudio())
                audio.add(track);
        }
        return audio.toArray(new MXFDemuxerTrack[0]);
    }

    public static class MXFDemuxerTrack implements SeekableDemuxerTrack {

        private UL essenceUL;
        private int dataLen;
        private int indexSegmentIdx;
        private int indexSegmentSubIdx;
        private int frameNo;
        private long pts;
        private int partIdx;
        private long partEssenceOffset;
        private GenericDescriptor descriptor;
        private TimelineTrack track;
        private boolean video;
        private boolean audio;
        private MXFCodec codec;
        private int audioFrameDuration;
        private int audioTimescale;
        private MXFDemuxer demuxer;

        public MXFDemuxerTrack(MXFDemuxer demuxer, UL essenceUL, TimelineTrack track, GenericDescriptor descriptor)
                throws IOException {
            this.demuxer = demuxer;
            this.essenceUL = essenceUL;
            this.track = track;
            this.descriptor = descriptor;

            if (descriptor instanceof GenericPictureEssenceDescriptor)
                video = true;
            else if (descriptor instanceof GenericSoundEssenceDescriptor)
                audio = true;
            codec = resolveCodec();

            if (codec != null || (descriptor instanceof WaveAudioDescriptor)) {
                Logger.warn("Track type: " + video + ", " + audio);

                if (audio && (descriptor instanceof WaveAudioDescriptor)) {
                    WaveAudioDescriptor wave = (WaveAudioDescriptor) descriptor;
                    cacheAudioFrameSizes(demuxer.ch);
                    audioFrameDuration = dataLen / ((wave.getQuantizationBits() >> 3) * wave.getChannelCount());
                    audioTimescale = (int) wave.getAudioSamplingRate().scalar();
                }
            }
        }

        public boolean isAudio() {
            return audio;
        }

        public boolean isVideo() {
            return video;
        }

        public double getDuration() {
            return demuxer.duration;
        }

        public int getNumFrames() {
            return demuxer.totalFrames;
        }

        public String getName() {
            return track.getName();
        }

        private void cacheAudioFrameSizes(SeekableByteChannel ch) throws IOException {
            for (MXFPartition mxfPartition : demuxer.partitions) {
                if (mxfPartition.getEssenceLength() > 0) {
                    ch.setPosition(mxfPartition.getEssenceFilePos());
                    KLV kl;
                    do {
                        kl = KLV.readKL(ch);
                        if (kl == null)
                            break;
                        ch.setPosition(ch.position() + kl.len);
                    } while (!essenceUL.equals(kl.key));

                    if (kl != null && essenceUL.equals(kl.key)) {
                        dataLen = (int) kl.len;
                        break;
                    }
                }
            }
        }

        @Override
        public Packet nextFrame() throws IOException {
            if (indexSegmentIdx >= demuxer.indexSegments.size())
                return null;

            IndexSegment seg = demuxer.indexSegments.get(indexSegmentIdx);

            long[] off = seg.getIe().getFileOff();
            int erDen = seg.getIndexEditRateNum();
            int erNum = seg.getIndexEditRateDen();

            long frameEssenceOffset = off[indexSegmentSubIdx];

            byte toff = seg.getIe().getDisplayOff()[indexSegmentSubIdx];
            boolean kf = seg.getIe().getKeyFrameOff()[indexSegmentSubIdx] == 0;

            while (frameEssenceOffset >= partEssenceOffset + demuxer.partitions.get(partIdx).getEssenceLength()
                    && partIdx < demuxer.partitions.size() - 1) {
                partEssenceOffset += demuxer.partitions.get(partIdx).getEssenceLength();
                partIdx++;
            }

            long frameFileOffset = frameEssenceOffset - partEssenceOffset
                    + demuxer.partitions.get(partIdx).getEssenceFilePos();

            Packet result;
            if (!audio) {
                result = readPacket(frameFileOffset, dataLen, pts + erNum * toff, erDen, erNum, frameNo++, kf);
                pts += erNum;
            } else {
                result = readPacket(frameFileOffset, dataLen, pts, audioTimescale, audioFrameDuration, frameNo++, kf);
                pts += audioFrameDuration;
            }

            indexSegmentSubIdx++;

            if (indexSegmentSubIdx >= off.length) {
                indexSegmentIdx++;
                indexSegmentSubIdx = 0;

                if (dataLen == 0 && indexSegmentIdx < demuxer.indexSegments.size()) {
                    IndexSegment nseg = demuxer.indexSegments.get(indexSegmentIdx);
                    pts = pts * nseg.getIndexEditRateNum() / erDen;
                }
            }

            return result;
        }

        public MXFPacket readPacket(long off, int len, long pts, int timescale, int duration, int frameNo, boolean kf)
                throws IOException {
            SeekableByteChannel ch = demuxer.ch;
            synchronized (ch) {
                ch.setPosition(off);

                KLV kl = KLV.readKL(ch);
                while (kl != null && !essenceUL.equals(kl.key)) {
                    ch.setPosition(ch.position() + kl.len);
                    kl = KLV.readKL(ch);
                }

                return kl != null && essenceUL.equals(kl.key)
                        ? new MXFPacket(NIOUtils.fetchFromChannel(ch, (int) kl.len), pts, timescale, duration, frameNo,
                                kf ? FrameType.KEY : FrameType.INTER, null, off, len)
                        : null;
            }
        }

        @Override
        public boolean gotoFrame(long frameNo) {
            if (frameNo == this.frameNo)
                return true;
            indexSegmentSubIdx = (int) frameNo;
            for (indexSegmentIdx = 0; indexSegmentIdx < demuxer.indexSegments.size()
                    && indexSegmentSubIdx >= demuxer.indexSegments.get(indexSegmentIdx)
                            .getIndexDuration(); indexSegmentIdx++) {
                indexSegmentSubIdx -= demuxer.indexSegments.get(indexSegmentIdx).getIndexDuration();
            }
            indexSegmentSubIdx = Math.min(indexSegmentSubIdx,
                    (int) demuxer.indexSegments.get(indexSegmentIdx).getIndexDuration());

            return true;
        }

        @Override
        public boolean gotoSyncFrame(long frameNo) {
            if (!gotoFrame(frameNo))
                return false;
            IndexSegment seg = demuxer.indexSegments.get(indexSegmentIdx);
            byte kfOff = seg.getIe().getKeyFrameOff()[indexSegmentSubIdx];
            return gotoFrame(frameNo + kfOff);
        }

        @Override
        public long getCurFrame() {
            return frameNo;
        }

        @Override
        public void seek(double second) {
            throw new NotSupportedException();
        }

        public UL getEssenceUL() {
            return essenceUL;
        }

        public GenericDescriptor getDescriptor() {
            return descriptor;
        }

        public MXFCodec getCodec() {
            return codec;
        }

        private MXFCodec resolveCodec() {
            UL codecUL;
            if (video)
                codecUL = ((GenericPictureEssenceDescriptor) descriptor).getPictureEssenceCoding();
            else if (audio)
                codecUL = ((GenericSoundEssenceDescriptor) descriptor).getSoundEssenceCompression();
            else
                return null;

            MXFCodec[] values = MXFCodec.values();
            for (int i = 0; i < values.length; i++) {
                MXFCodec codec = values[i];
                if (codec.getUl().equals(codecUL))
                    return codec;
            }
            Logger.warn("Unknown codec: " + codecUL);

            return null;
        }

        public int getTrackId() {
            return track.getTrackId();
        }

        @Override
        public DemuxerTrackMeta getMeta() {
            Size size = null;
            if (video) {
                GenericPictureEssenceDescriptor pd = (GenericPictureEssenceDescriptor) descriptor;
                size = new Size(pd.getStoredWidth(), pd.getStoredHeight());
            }

            TrackType t = video ? TrackType.VIDEO : (audio ? TrackType.AUDIO : TrackType.OTHER);
            return new DemuxerTrackMeta(t, getCodec().getCodec(), demuxer.duration, null, demuxer.totalFrames, null,
                    org.jcodec.common.VideoCodecMeta.createSimpleVideoCodecMeta(size, ColorSpace.YUV420), null);
        }

        public Rational getEditRate() {
            return this.track.getEditRate();
        }
    }

    public static class MXFPacket extends Packet {
        private long offset;
        private int len;

        public MXFPacket(ByteBuffer data, long pts, int timescale, long duration, long frameNo, FrameType frameType,
                TapeTimecode tapeTimecode, long offset, int len) {
            super(data, pts, timescale, duration, frameNo, frameType, tapeTimecode, 0);
            this.offset = offset;
            this.len = len;
        }

        public long getOffset() {
            return offset;
        }

        public int getLen() {
            return len;
        }
    }

    /**
     * Fast loading version of demuxer, doesn't search for metadata in ALL the
     * partitions, only the header and footer are being inspected
     */
    public static class Fast extends MXFDemuxer {

        public Fast(SeekableByteChannel ch) throws IOException {
            super(ch);
        }

        @Override
        public void parseHeader(SeekableByteChannel ff) throws IOException {
            partitions = new ArrayList();
            metadata = new ArrayList();

            header = readHeaderPartition(ff);
            metadata.addAll(readPartitionMeta(ff, header));
            partitions.add(header);

            ff.setPosition(header.getPack().getFooterPartition());
            KLV kl = KLV.readKL(ff);
            if (kl != null) {
                ByteBuffer fetchFrom = NIOUtils.fetchFromChannel(ff, (int) kl.len);
                MXFPartition footer = MXFPartition.read(kl.key, fetchFrom, ff.position() - kl.offset, ff.size());

                metadata.addAll(readPartitionMeta(ff, footer));
            }
        }
    }

    public List getSourceClips(int trackId) {
        boolean trackFound = true;
        List clips = new ArrayList();
        for (MXFMetadata m : metadata) {
            if (m instanceof TimelineTrack) {
                TimelineTrack tt = (TimelineTrack) m;
                int trackId2 = tt.getTrackId();
                trackFound = (trackId2 == trackId);
            }
            if (trackFound && m instanceof SourceClip) {
                SourceClip clip = (SourceClip) m;
                if (clip.getSourceTrackId() == trackId) {
                    clips.add(clip);
                }
            }
        }
        return clips;
    }

    public static TapeTimecode readTapeTimecode(File mxf) throws IOException {
        FileChannelWrapper read = NIOUtils.readableChannel(mxf);
        try {
            Fast fast = new Fast(read);
            MXFDemuxerTrack track = fast.getVideoTrack();
            TimecodeComponent timecode = fast.getTimecode();
            List sourceClips = fast.getSourceClips(track.getTrackId());
            long tc = 0;
            boolean dropFrame = false;
            Rational editRate = null;
            if (timecode != null) {
                editRate = track.getEditRate();
                dropFrame = timecode.getDropFrame() != 0;
                tc = timecode.getStart();
            }
            for (SourceClip sourceClip : sourceClips) {
                tc += sourceClip.getStartPosition();
            }
    
            if (editRate != null) {
                return TapeTimecode.tapeTimecode(tc, dropFrame, (int) Math.ceil(editRate.toDouble()));
            }
            return null;
        } finally {
            read.close();
        }
    }

    public List getMetadata() {
        return unmodifiableList(metadata);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy