package org.jcodec.containers.mxf;
import static org.jcodec.containers.mxf.MXFConst.klMetadataMapping;
import static org.jcodec.containers.mxf.model.MXFUtil.findAllMeta;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.jcodec.api.NotSupportedException;
import org.jcodec.common.DemuxerTrackMeta;
import org.jcodec.common.SeekableDemuxerTrack;
import org.jcodec.common.TrackType;
import org.jcodec.common.VideoCodecMeta;
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.containers.mxf.MXFConst.MXFCodecMapping;
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.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<MXFMetadata> metadata;
protected MXFPartition header;
protected List<MXFPartition> partitions;
protected List<IndexSegment> 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;
}
private MXFDemuxerTrack[] findTracks() throws IOException {
List<MXFDemuxerTrack> rt = new ArrayList<MXFDemuxerTrack>();
List<TimelineTrack> tracks = findAllMeta(metadata, TimelineTrack.class);
List<FileDescriptor> descriptors = findAllMeta(metadata, FileDescriptor.class);
for (TimelineTrack track : tracks) {
if (track.getTrackNumber() != 0) {
int trackNumber = track.getTrackNumber();
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;
}
MXFDemuxerTrack dt = createTrack(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), track, descriptor);
if (dt.getCodec() != null || (descriptor instanceof WaveAudioDescriptor))
rt.add(dt);
}
}
return rt.toArray(new MXFDemuxerTrack[0]);
}
public static FileDescriptor findDescriptor(List<FileDescriptor> 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<IndexSegment> getIndexes() {
return indexSegments;
}
public List<MXFPartition> getEssencePartitions() {
return partitions;
}
public TimecodeComponent getTimecode() {
return timecode;
}
public void parseHeader(SeekableByteChannel ff) throws IOException {
KLV kl;
header = readHeaderPartition(ff);
metadata = new ArrayList<MXFMetadata>();
partitions = new ArrayList<MXFPartition>();
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<MXFMetadata> readPartitionMeta(SeekableByteChannel ff, MXFPartition header) throws IOException {
KLV kl;
long basePos = ff.position();
List<MXFMetadata> local = new ArrayList<MXFMetadata>();
ByteBuffer metaBuffer = NIOUtils.fetchFromChannel(ff, (int) Math.max(0, header.getEssenceFilePos() - basePos));
while (metaBuffer.hasRemaining() && (kl = KLV.readKLFromBuffer(metaBuffer, basePos)) != null) {
MXFMetadata meta = parseMeta(kl.key, NIOUtils.read(metaBuffer, (int) kl.len));
if (meta != null)
local.add(meta);
}
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<? extends MXFMetadata> class1 = klMetadataMapping.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<IndexSegment>();
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<MXFDemuxerTrack> audio = new ArrayList<MXFDemuxerTrack>();
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 MXFCodecMapping 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 MXFCodecMapping getCodec() {
return codec;
}
private MXFCodecMapping resolveCodec() {
UL codecUL;
if (video)
codecUL = ((GenericPictureEssenceDescriptor) descriptor).getPictureEssenceCoding();
else if (audio)
codecUL = ((GenericSoundEssenceDescriptor) descriptor).getSoundEssenceCompression();
else
return null;
MXFCodecMapping[] values = MXFConst.MXFCodecMapping.values();
for (int i = 0; i < values.length; i++) {
MXFCodecMapping codec = values[i];
if (codec.getUl().maskEquals(codecUL, 0xff7f))
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,
new VideoCodecMeta(size, ColorSpace.YUV420), null);
}
}
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);
}
public void parseHeader(SeekableByteChannel ff) throws IOException {
partitions = new ArrayList<MXFPartition>();
metadata = new ArrayList<MXFMetadata>();
header = readHeaderPartition(ff);
metadata.addAll(readPartitionMeta(ff, header));
partitions.add(header);
ff.setPosition(header.getPack().getFooterPartition());
KLV kl = KLV.readKL(ff);
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));
}
}
}