package org.jcodec.containers.mkv;
import static org.jcodec.common.io.IOUtils.readFileToByteArray;
import static org.jcodec.containers.mkv.MKVType.Audio;
import static org.jcodec.containers.mkv.MKVType.Channels;
import static org.jcodec.containers.mkv.MKVType.CodecID;
import static org.jcodec.containers.mkv.MKVType.Cues;
import static org.jcodec.containers.mkv.MKVType.DisplayHeight;
import static org.jcodec.containers.mkv.MKVType.DisplayWidth;
import static org.jcodec.containers.mkv.MKVType.Duration;
import static org.jcodec.containers.mkv.MKVType.Info;
import static org.jcodec.containers.mkv.MKVType.Language;
import static org.jcodec.containers.mkv.MKVType.MuxingApp;
import static org.jcodec.containers.mkv.MKVType.Name;
import static org.jcodec.containers.mkv.MKVType.PixelHeight;
import static org.jcodec.containers.mkv.MKVType.PixelWidth;
import static org.jcodec.containers.mkv.MKVType.SamplingFrequency;
import static org.jcodec.containers.mkv.MKVType.SeekID;
import static org.jcodec.containers.mkv.MKVType.SeekPosition;
import static org.jcodec.containers.mkv.MKVType.Segment;
import static org.jcodec.containers.mkv.MKVType.Tags;
import static org.jcodec.containers.mkv.MKVType.TimecodeScale;
import static org.jcodec.containers.mkv.MKVType.TrackEntry;
import static org.jcodec.containers.mkv.MKVType.TrackNumber;
import static org.jcodec.containers.mkv.MKVType.TrackType;
import static org.jcodec.containers.mkv.MKVType.TrackUID;
import static org.jcodec.containers.mkv.MKVType.Tracks;
import static org.jcodec.containers.mkv.MKVType.WritingApp;
import static org.jcodec.containers.mkv.boxes.EbmlUint.longToBytes;
import static org.jcodec.containers.mkv.muxer.MKVMuxer.createBuffer;
import static org.jcodec.containers.mkv.muxer.MKVMuxer.createDouble;
import static org.jcodec.containers.mkv.muxer.MKVMuxer.createLong;
import static org.jcodec.containers.mkv.muxer.MKVMuxer.createString;
import org.jcodec.api.UnhandledStateException;
import org.jcodec.common.io.FileChannelWrapper;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mkv.SeekHeadFactory.SeekMock;
import org.jcodec.containers.mkv.boxes.EbmlBase;
import org.jcodec.containers.mkv.boxes.EbmlBin;
import org.jcodec.containers.mkv.boxes.EbmlDate;
import org.jcodec.containers.mkv.boxes.EbmlMaster;
import org.jcodec.containers.mkv.boxes.EbmlString;
import org.jcodec.containers.mkv.boxes.EbmlUint;
import org.jcodec.containers.mkv.muxer.MKVMuxer;
import org.jcodec.containers.mkv.util.EbmlUtil;
import org.junit.Assert;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.System;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
public class SeekHeadFactoryTest {
@Test
public void testJCodecConvention() throws IOException {
List<EbmlMaster> tree = fakeMkvTree();
FileOutputStream fos = new FileOutputStream("src/test/resources/mkv/seek_head.ebml");
try {
SeekableByteChannel fc = new FileChannelWrapper(fos.getChannel());
for (EbmlMaster me : tree)
me.mux(fc);
} finally {
IOUtils.closeQuietly(fos);
}
}
private List<EbmlMaster> fakeMkvTree() {
List<EbmlMaster> tree = new ArrayList<EbmlMaster>();
EbmlMaster ebmlHeaderElem = (EbmlMaster) MKVType.createByType(MKVType.EBML);
addFakeEbmlHeader(ebmlHeaderElem);
tree.add(ebmlHeaderElem);
// # Segment
EbmlMaster segmentElem = (EbmlMaster) MKVType.createByType(Segment);
addFakeInfo(segmentElem);
// Tracks Info
addFakeTracks(segmentElem);
// Chapters
// Attachments
// Tags
// Cues
addFakeCues(segmentElem);
// # Meta Seek
muxSeeks(segmentElem);
tree.add(segmentElem);
return tree;
}
private void muxSeeks(EbmlMaster segmentElem) {
SeekHeadFactory shi = new SeekHeadFactory();
for (int i = 0; i < segmentElem.children.size() && i < 4; i++) {
EbmlBase e = segmentElem.children.get(i);
if (!e.type.equals(Tracks) && !e.type.equals(Info) && !e.type.equals(Tags) && !e.type.equals(Cues)) {
throw new UnhandledStateException("Unknown entry found among segment children, index " + i);
}
shi.add(e);
}
segmentElem.children.add(0, shi.indexSeekHead());
}
public void testMuxingFromTestFileSeek() throws Exception {
File file = new File("src/test/resources/mkv/seek_head.ebml");
byte[] rawFrame = readFileToByteArray(file);
EbmlMaster seekHead = MKVType.createByType(MKVType.SeekHead);
EbmlMaster seek = MKVType.createByType(MKVType.Seek);
createBuffer(seek, SeekID, ByteBuffer.wrap(Info.id));
EbmlUint se = (EbmlUint) MKVType.createByType(SeekPosition);
se.setUint(64);
seek.add(se);
seekHead.add(seek);
seek = MKVType.createByType(MKVType.Seek);
createBuffer(seek, SeekID, ByteBuffer.wrap(Tracks.id));
se = (EbmlUint) MKVType.createByType(SeekPosition);
se.setUint(275);
seek.add(se);
seekHead.add(seek);
seek = MKVType.createByType(MKVType.Seek);
createBuffer(seek, SeekID, ByteBuffer.wrap(Tags.id));
se = (EbmlUint) MKVType.createByType(SeekPosition);
se.setUint(440);
seek.add(se);
seekHead.add(seek);
seek = MKVType.createByType(MKVType.Seek);
createBuffer(seek, SeekID, ByteBuffer.wrap(Cues.id));
se = (EbmlUint) MKVType.createByType(SeekPosition);
se.setUint(602);
seek.add(se);
seekHead.add(seek);
ByteBuffer bb = seekHead.getData();
Assert.assertArrayEquals(rawFrame, bb.array());
Assert.assertArrayEquals(rawFrame, NIOUtils.toArray(bb));
}
@Test
public void printSizeOptions() throws Exception {
long s = SeekHeadFactory.estimeteSeekSize(4, 1); // seek to info
s += SeekHeadFactory.estimeteSeekSize(4, 1);
s += SeekHeadFactory.estimeteSeekSize(4, 2);
s += SeekHeadFactory.estimeteSeekSize(4, 3);
int z = EbmlUtil.ebmlLength(s);
s += 4 + z;
System.out.println("size:" + s + " can be encoded in " + z + " byte(s) using ebml, with seekhead size eq to:" + z);
}
@Test
public void printVariousSeekSizes() throws Exception {
System.out.println("seekSize(1): "+SeekHeadFactory.estimeteSeekSize(4, 1));
System.out.println("seekSize(2): "+SeekHeadFactory.estimeteSeekSize(4, 2));
System.out.println("seekSize(3): "+SeekHeadFactory.estimeteSeekSize(4, 3));
}
public static int fakeZOffset = 0;
public static SeekMock createFakeZ(byte id[], int size){
SeekMock z = new SeekMock();
z.id = id;
z.size = size;
z.dataOffset = fakeZOffset;
z.seekPointerSize = longToBytes(z.dataOffset).length;
fakeZOffset += z.size;
System.out.println("Added id:"+EbmlUtil.toHexString(z.id)+" offset:"+z.dataOffset+" seekpointer size:"+z.seekPointerSize);
return z;
}
@Test
public void testEdgeCasesWithFakeZ() throws Exception {
SeekHeadFactory a = new SeekHeadFactory();
a.a.add(createFakeZ(Info.id, 0xFF));
a.a.add(createFakeZ(Tracks.id, 0xFF05));
a.a.add(createFakeZ(Tags.id, 0xFEFFC0));
a.a.add(createFakeZ(Cues.id, 0xFF));
int computeSize = a.computeSeekHeadSize();
System.out.println("SeekHeadSize: "+computeSize);
Assert.assertEquals(a.estimateSize(), computeSize);
}
public static EbmlBase createFakeElement(byte[] id, int size){
EbmlBase e = new EbmlBin(id);
e.dataLen = size;
return e;
}
@Test
public void testSeekHeadSize() throws Exception {
SeekHeadFactory a = new SeekHeadFactory();
a.add(createFakeElement(Info.id, 0xFF-4-2));
a.add(createFakeElement(Tracks.id, 0xFF05-4-2));
a.add(createFakeElement(Cues.id, 0xFEFFFF-4-3));
int computeSize = a.computeSeekHeadSize();
System.out.println("SeekHeadSize: "+computeSize);
Assert.assertEquals(a.estimateSize(), computeSize);
}
@Test
public void testSeekHeadMuxing() throws Exception {
SeekHeadFactory a = new SeekHeadFactory();
a.add(createFakeElement(Info.id, 0xFF-4-2));
a.add(createFakeElement(Tracks.id, 0xFF00-4-3));
a.add(createFakeElement(Cues.id, 0xFF0000-4-4));
a.add(createFakeElement(Tags.id, 0xFF-4-4));
int computeSize = a.computeSeekHeadSize();
System.out.println("SeekHeadSize: "+computeSize);
Assert.assertEquals(a.estimateSize(), computeSize);
ByteBuffer mux = a.indexSeekHead().getData();
Assert.assertEquals(a.estimateSize(), mux.limit());
FileOutputStream fos = new FileOutputStream("src/test/resources/mkv/seek_head.ebml");
try {
fos.getChannel().write(mux);
} finally {
IOUtils.closeQuietly(fos);
}
}
private void addFakeEbmlHeader(EbmlMaster ebmlHeaderElem) {
EbmlString docTypeElem = (EbmlString) MKVType.createByType(MKVType.DocType);
docTypeElem.setString("matroska");
EbmlUint docTypeVersionElem = (EbmlUint) MKVType.createByType(MKVType.DocTypeVersion);
docTypeVersionElem.setUint(2);
EbmlUint docTypeReadVersionElem = (EbmlUint) MKVType.createByType(MKVType.DocTypeReadVersion);
docTypeReadVersionElem.setUint(2);
ebmlHeaderElem.add(docTypeElem);
ebmlHeaderElem.add(docTypeVersionElem);
ebmlHeaderElem.add(docTypeReadVersionElem);
}
private void addFakeInfo(EbmlMaster segmentElem) {
// # Segment Info
EbmlMaster segmentInfoElem = (EbmlMaster) MKVType.createByType(MKVType.Info);
EbmlDate dateElem = (EbmlDate) MKVType.createByType(MKVType.DateUTC);
dateElem.setDate(new Date());
segmentInfoElem.add(dateElem);
// Add timecode scale
createLong(segmentInfoElem, TimecodeScale, 100000);
createDouble(segmentInfoElem, Duration, 87336.0 * 1000.0);
createString(segmentInfoElem, WritingApp, "JCodec v0.1.0");
createString(segmentInfoElem, MuxingApp, "Matroska Muxer v0.1a");
segmentElem.add(segmentInfoElem);
}
private void addFakeTracks(EbmlMaster segmentElem) {
Random r = new Random();
EbmlMaster tracksElem = (EbmlMaster) MKVType.createByType(Tracks);
EbmlMaster trackEntryElem = (EbmlMaster) MKVType.createByType(TrackEntry);
createLong(trackEntryElem, TrackNumber, 1);
createLong(trackEntryElem, TrackUID, r.nextLong());
createLong(trackEntryElem, TrackType, 1); // Video
createString(trackEntryElem, Name, "Video");
createString(trackEntryElem, Language, "en");
createString(trackEntryElem, CodecID, "V_UNCOMPRESSED");
// Now we add the audio/video dependant sub-elements
EbmlMaster trackVideoElem = (EbmlMaster) MKVType.createByType(MKVType.Video);
createLong(trackVideoElem, PixelWidth, 1024);
createLong(trackVideoElem, PixelHeight, 768);
createLong(trackVideoElem, DisplayWidth, 1024);
createLong(trackVideoElem, DisplayHeight, 768);
trackEntryElem.add(trackVideoElem);
tracksElem.add(trackEntryElem);
trackEntryElem = (EbmlMaster) MKVType.createByType(TrackEntry);
createLong(trackEntryElem, TrackNumber, 2);
createLong(trackEntryElem, TrackUID, r.nextLong());
createLong(trackEntryElem, TrackType, 2); // Video
createString(trackEntryElem, Name, "Audio");
createString(trackEntryElem, Language, "en");
createString(trackEntryElem, CodecID, "A_MPEG/L3");
EbmlMaster trackAudioElem = (EbmlMaster) MKVType.createByType(Audio);
createLong(trackAudioElem, Channels, 2);
MKVMuxer.createDouble(trackAudioElem, SamplingFrequency, 48000.0);
trackEntryElem.add(trackAudioElem);
tracksElem.add(trackEntryElem);
segmentElem.add(tracksElem);
}
private void addFakeCues(EbmlMaster segmentElem) {
EbmlMaster cuesElem = (EbmlMaster) MKVType.createByType(Cues);
segmentElem.add(cuesElem);
}
}