diff --git a/io/src/main/java/org/red5/io/isobmff/atom/HVC1Box.java b/io/src/main/java/org/red5/io/isobmff/atom/HVC1Box.java new file mode 100644 index 0000000000000000000000000000000000000000..f79daef0a11666b89f6089d9c19539413c745c5d --- /dev/null +++ b/io/src/main/java/org/red5/io/isobmff/atom/HVC1Box.java @@ -0,0 +1,12 @@ +package org.red5.io.isobmff.atom; + +import org.jcodec.containers.mp4.boxes.Header; +import org.jcodec.containers.mp4.boxes.VideoSampleEntry; + +public class HVC1Box extends VideoSampleEntry { + + public HVC1Box() { + super(new Header("hvc1")); + } + +} diff --git a/io/src/main/java/org/red5/io/isobmff/atom/ShortEsdsBox.java b/io/src/main/java/org/red5/io/isobmff/atom/ShortEsdsBox.java new file mode 100644 index 0000000000000000000000000000000000000000..9df7080c1bed80798cf0be92e7ea0cc6b357f074 --- /dev/null +++ b/io/src/main/java/org/red5/io/isobmff/atom/ShortEsdsBox.java @@ -0,0 +1,105 @@ +package org.red5.io.isobmff.atom; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.jcodec.codecs.mpeg4.es.DecoderConfig; +import org.jcodec.codecs.mpeg4.es.DecoderSpecific; +import org.jcodec.codecs.mpeg4.es.Descriptor; +import org.jcodec.codecs.mpeg4.es.DescriptorParser; +import org.jcodec.codecs.mpeg4.es.ES; +import org.jcodec.codecs.mpeg4.es.NodeDescriptor; +import org.jcodec.codecs.mpeg4.es.SL; +import org.jcodec.containers.mp4.boxes.FullBox; +import org.jcodec.containers.mp4.boxes.Header; + +public class ShortEsdsBox extends FullBox { + + private ByteBuffer streamInfo; + + private int objectType; + + private int bufSize; + + private int maxBitrate; + + private int avgBitrate; + + private int trackId; + + public static String fourcc() { + return "esds"; + } + + public ShortEsdsBox(Header atom) { + super(atom); + } + + @Override + protected void doWrite(ByteBuffer out) { + super.doWrite(out); + if (streamInfo != null && streamInfo.remaining() > 0) { + ArrayList<Descriptor> l = new ArrayList<Descriptor>(); + ArrayList<Descriptor> l1 = new ArrayList<Descriptor>(); + l1.add(new DecoderSpecific(streamInfo)); + l.add(new DecoderConfig(objectType, bufSize, maxBitrate, avgBitrate, l1)); + l.add(new SL()); + new ES(trackId, l).write(out); + } else { + ArrayList<Descriptor> l = new ArrayList<Descriptor>(); + l.add(new DecoderConfig(objectType, bufSize, maxBitrate, avgBitrate, new ArrayList<Descriptor>())); + l.add(new SL()); + new ES(trackId, l).write(out); + } + } + + @Override + public int estimateSize() { + return 64; + } + + @Override + public void parse(ByteBuffer input) { + super.parse(input); + ES es = (ES) DescriptorParser.read(input); + trackId = es.getTrackId(); + DecoderConfig decoderConfig = NodeDescriptor.findByTag(es, DecoderConfig.tag()); + objectType = decoderConfig.getObjectType(); + bufSize = decoderConfig.getBufSize(); + maxBitrate = decoderConfig.getMaxBitrate(); + avgBitrate = decoderConfig.getAvgBitrate(); + DecoderSpecific decoderSpecific = NodeDescriptor.findByTag(decoderConfig, DecoderSpecific.tag()); + if (decoderSpecific != null) { + streamInfo = decoderSpecific.getData(); + } + } + + public boolean hasStreamInfo() { + return streamInfo != null; + } + + public ByteBuffer getStreamInfo() { + return streamInfo; + } + + public int getObjectType() { + return objectType; + } + + public int getBufSize() { + return bufSize; + } + + public int getMaxBitrate() { + return maxBitrate; + } + + public int getAvgBitrate() { + return avgBitrate; + } + + public int getTrackId() { + return trackId; + } + +} diff --git a/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java b/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java index 0f4cd79342b640babd32b658b4ed0ea7eb65344d..1a4bf5208bc95b1cb40b240e79300166089fa9b3 100644 --- a/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java +++ b/io/src/main/java/org/red5/io/mp4/impl/MP4Reader.java @@ -25,16 +25,19 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.mina.core.buffer.IoBuffer; +import org.jcodec.codecs.h264.mp4.AvcCBox; import org.jcodec.codecs.mpeg4.mp4.EsdsBox; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.containers.mp4.MP4TrackType; import org.jcodec.containers.mp4.MP4Util; import org.jcodec.containers.mp4.MP4Util.Movie; +import org.jcodec.containers.mp4.boxes.AVC1Box; import org.jcodec.containers.mp4.boxes.AudioSampleEntry; import org.jcodec.containers.mp4.boxes.Box; import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box; import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox; +import org.jcodec.containers.mp4.boxes.ColorExtension; import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox; import org.jcodec.containers.mp4.boxes.HandlerBox; import org.jcodec.containers.mp4.boxes.MediaBox; @@ -43,17 +46,18 @@ import org.jcodec.containers.mp4.boxes.MediaInfoBox; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.boxes.MovieHeaderBox; import org.jcodec.containers.mp4.boxes.NodeBox; +import org.jcodec.containers.mp4.boxes.PixelAspectExt; import org.jcodec.containers.mp4.boxes.SampleDescriptionBox; import org.jcodec.containers.mp4.boxes.SampleSizesBox; import org.jcodec.containers.mp4.boxes.SampleToChunkBox; +import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry; import org.jcodec.containers.mp4.boxes.SyncSamplesBox; import org.jcodec.containers.mp4.boxes.TimeToSampleBox; +import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; import org.jcodec.containers.mp4.boxes.TrackHeaderBox; import org.jcodec.containers.mp4.boxes.TrakBox; import org.jcodec.containers.mp4.boxes.VideoSampleEntry; import org.jcodec.containers.mp4.boxes.WaveExtension; -import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry; -import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; import org.red5.io.IStreamableFile; import org.red5.io.ITag; import org.red5.io.ITagReader; @@ -61,6 +65,8 @@ import org.red5.io.IoConstants; import org.red5.io.amf.Output; import org.red5.io.flv.IKeyFrameDataAnalyzer; import org.red5.io.flv.impl.Tag; +import org.red5.io.isobmff.atom.HVC1Box; +import org.red5.io.isobmff.atom.ShortEsdsBox; import org.red5.io.mp4.MP4Frame; import org.red5.io.utils.HexDump; import org.slf4j.Logger; @@ -343,7 +349,7 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer "tag": "stsd", "boxes": [ { - "tag": "mp4a | mp4v | avc1 | hvc1", + "tag": "mp4a | ac-3 | mp4v | avc1 | hvc1", "boxes": [ { "tag": "esds" @@ -358,12 +364,27 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer audioCodecId = "mp4a"; processAudioSampleEntry((AudioSampleEntry) stbox, scale.get()); break; + case "ac-3": + audioCodecId = "ac-3"; + processAudioSampleEntry((AudioSampleEntry) stbox, scale.get()); + break; case "mp4v": videoCodecId = "mp4v"; processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); break; case "avc1": videoCodecId = "avc1"; + //AVC1Box avc1 = Box.asBox(AVC1Box.class, stbox); + processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); + break; + case "hev1": + videoCodecId = "hev1"; + //HEV1Box hev1 = Box.asBox(HEV1Box.class, stbox); + //processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); + break; + case "hvc1": + videoCodecId = "hvc1"; + //HVC1Box hvc1 = Box.asBox(HVC1Box.class, stbox); processVideoSampleEntry((VideoSampleEntry) stbox, scale.get()); break; default: @@ -488,7 +509,8 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer case "ctts": // ctts - (composition) time to sample log.debug("Composition time to sample atom found"); CompositionOffsetsBox ctts = (CompositionOffsetsBox) sbox; - compositionTimes = List.of(ctts.getEntries()); + compositionTimes = new LinkedList<>(); + compositionTimes.addAll(List.of(ctts.getEntries())); log.debug("Record count: {}", compositionTimes.size()); if (log.isTraceEnabled()) { for (CompositionOffsetsBox.Entry rec : compositionTimes) { @@ -572,13 +594,17 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer log.debug("Audio sample entry box: {}", box); switch (box.getFourcc()) { case "esds": - if (box.estimateSize() > 0) { - - } - EsdsBox esds = Box.asBox(EsdsBox.class, box); + long esdsBodySize = box.getHeader().getBodySize(); + log.debug("esds body size: {}", esdsBodySize); + // less than 28 bytes doesn't contain DecoderSpecific content + ShortEsdsBox esds = Box.asBox(ShortEsdsBox.class, box); log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", esds.getFourcc(), esds.getObjectType(), esds.getAvgBitrate(), esds.getMaxBitrate()); // http://stackoverflow.com/questions/3987850/mp4-atom-how-to-discriminate-the-audio-codec-is-it-aac-or-mp3 - audioDecoderBytes = esds.getStreamInfo().array(); + if (esds.hasStreamInfo()) { + audioDecoderBytes = esds.getStreamInfo().array(); + } else { + audioDecoderBytes = EMPTY_AAC; + } log.debug("Audio config bytes: {}", HexDump.byteArrayToHexString(audioDecoderBytes)); // the first 5 (0-4) bits tell us about the coder used for aacaot/aottype http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio 0 - NULL 1 - AAC Main (a deprecated AAC profile // from MPEG-2) 2 - AAC LC or backwards compatible HE-AAC 3 - AAC Scalable Sample Rate 4 - AAC LTP (a replacement for AAC Main, rarely used) 5 - HE-AAC explicitly signaled @@ -617,14 +643,17 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer } log.debug("Audio coder type: {} {} id: {}", audioCoderType, Integer.toBinaryString(audioCoderType), audioCodecId); break; + case "dac3": // {"tag":"ac-3","boxes": [{"tag":"dac3"}]} + //AC3SpecificBox dac3 = Box.asBox(AC3SpecificBox.class, box); + break; case "wave": // check for decompression param atom WaveExtension wave = Box.asBox(WaveExtension.class, box); log.debug("wave atom found"); // wave/esds - esds = wave.getBoxes().stream().filter(b -> b instanceof EsdsBox).map(b -> (EsdsBox) b).findFirst().orElse(null); - if (esds != null) { - log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", esds.getFourcc(), esds.getObjectType(), esds.getAvgBitrate(), esds.getMaxBitrate()); + EsdsBox wesds = wave.getBoxes().stream().filter(b -> b instanceof EsdsBox).map(b -> (EsdsBox) b).findFirst().orElse(null); + if (wesds != null) { + log.debug("Process {} obj: {} avg bitrate: {} max bitrate: {}", wesds.getFourcc(), wesds.getObjectType(), wesds.getAvgBitrate(), wesds.getMaxBitrate()); } else { log.debug("esds not found in wave"); // mp4a/esds @@ -632,6 +661,9 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer //esds = mp4a.getBoxes(ESDescriptorBox.class).get(0); } break; + case "btrt": + //BitRateBox btrt = Box.asBox(BitRateBox.class, box); + break; default: log.warn("Unhandled sample desc extension: {}", box); break; @@ -661,37 +693,34 @@ public class MP4Reader implements IoConstants, ITagReader, IKeyFrameDataAnalyzer videoDecoderBytes = esds.getStreamInfo().array(); log.debug("Video config bytes: {}", HexDump.byteArrayToHexString(videoDecoderBytes)); break; - /* - stsd child: {"tag":"avc1","boxes": [{"tag":"avcC"},{"tag":"btrt"}]} - Compressor: frame count: 1 - Video sample entry box: {"tag":"avcC"} - Unhandled sample desc extension: {"tag":"avcC"} - Video sample entry box: {"tag":"btrt"} - Unhandled sample desc extension: {"tag":"btrt"} - - - case "avcC": // videoCodecId = "avc1" - AvcConfigurationBox avc1 = vse.getBoxes(AvcConfigurationBox.class).get(0); - avcLevel = avc1.getAvcLevelIndication(); - log.debug("AVC level: {}", avcLevel); - avcProfile = avc1.getAvcProfileIndication(); - log.debug("AVC Profile: {}", avcProfile); - AvcDecoderConfigurationRecord avcC = avc1.getavcDecoderConfigurationRecord(); - if (avcC != null) { - long videoConfigContentSize = avcC.getContentSize(); - log.debug("AVCC size: {}", videoConfigContentSize); - ByteBuffer byteBuffer = ByteBuffer.allocate((int) videoConfigContentSize); - avc1.avcDecoderConfigurationRecord.getContent(byteBuffer); - byteBuffer.flip(); - videoDecoderBytes = new byte[byteBuffer.limit()]; - byteBuffer.get(videoDecoderBytes); - } else { - // quicktime and ipods use a pixel aspect atom (pasp) - // since we have no avcC check for this and avcC may be a child - log.warn("avcC atom not found; we may need to modify this to support pasp atom"); - } + case "avcC": // videoCodecId = "avc1" + //AVC1Box avc1 = Box.asBox(AVC1Box.class, vse); + AvcCBox avcC = Box.asBox(AvcCBox.class, box); + avcLevel = avcC.getLevel(); + avcProfile = avcC.getProfile(); + log.debug("Process {} level: {} nal len: {} profile: {} compat: {}", avcC.getFourcc(), avcLevel, avcC.getNalLengthSize(), avcProfile, avcC.getProfileCompat()); + avcC.getSpsList().forEach(sps -> log.debug("SPS: {}", sps)); + avcC.getPpsList().forEach(pps -> log.debug("PPS: {}", pps)); + break; + case "hvcC": // videoCodecId = "hvc1" + // hvc1 size 682 offset 581 + //HVC1Box avc1 = Box.asBox(HVC1Box.class, vse); + // hvcC size 574 offset 667 + //HvcCBox hvcC = Box.asBox(HvcCBox.class, box); + //HvcCBox hvcC = Box.asBox(HvcCBox.class, box); + break; + case "btrt": + //BitRateBox btrt = Box.asBox(BitRateBox.class, box); + break; + case "pasp": + PixelAspectExt pasp = Box.asBox(PixelAspectExt.class, box); + log.debug("Process {} hSpacing: {} vSpacing: {}", pasp.getFourcc(), pasp.gethSpacing(), pasp.getvSpacing()); + break; + case "colr": // color + // colr size 18 offset 1241 + ColorExtension colr = Box.asBox(ColorExtension.class, box); + log.debug("Process {} primaries: {} transfer: {} matrix: {}", colr.getFourcc(), colr.getPrimariesIndex(), colr.getTransferFunctionIndex(), colr.getMatrixIndex()); break; - */ default: log.warn("Unhandled sample desc extension: {}", box); break; diff --git a/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java b/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java index 2bf3652e10e5442a97d0063ca936f37d523469ff..d4c0f6a5179c9157127c3fecbaa1e9639dd868ca 100644 --- a/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java +++ b/io/src/test/java/org/red5/io/mp4/impl/MP4ReaderTest.java @@ -26,10 +26,12 @@ public class MP4ReaderTest extends TestCase { @Test public void testCtor() throws Exception { // use for the internal unit tests - //File file = new File("target/test-classes/fixtures/bbb.mp4"); - File file = new File("/media/mondain/terrorbyte/Videos/bbb_sunflower_2160p_60fps_normal.mp4"); - //File file = new File("target/test-classes/fixtures/sample.mp4"); - //File file = new File("target/test-classes/fixtures/MOV1.MOV"); + //File file = new File("target/test-classes/fixtures/bbb.mp4"); // non-avc1 h264 video / aac audio + File file = new File("target/test-classes/fixtures/mov_h264.mp4"); // avc1 h264 video / aac audio + //File file = new File("target/test-classes/fixtures/mov_h265.mp4"); // hev1 h265 video / aac audio + //File file = new File("/media/mondain/terrorbyte/Videos/bbb_sunflower_2160p_60fps_normal.mp4"); // h264 video / ac-3 audio + //File file = new File("target/test-classes/fixtures/sample.mp4"); // non-avc1 h264 video / aac audio + //File file = new File("target/test-classes/fixtures/MOV1.MOV"); // hcv1 h265 video / aac audio // test clips for issues/bugs // https://code.google.com/p/red5/issues/detail?id=141 //File file = new File("target/test-classes/fixtures/test_480_aac.f4v"); diff --git a/io/src/test/resources/fixtures/ffmpeg b/io/src/test/resources/fixtures/ffmpeg new file mode 100755 index 0000000000000000000000000000000000000000..a138f80301f58e657a51e48a4d06bb2d6fd49094 Binary files /dev/null and b/io/src/test/resources/fixtures/ffmpeg differ diff --git a/io/src/test/resources/fixtures/mov_h264.mp4 b/io/src/test/resources/fixtures/mov_h264.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..c1d04bd619702c67db99dbc0a8c8817eba0ceddb Binary files /dev/null and b/io/src/test/resources/fixtures/mov_h264.mp4 differ diff --git a/io/src/test/resources/fixtures/mov_h265.mp4 b/io/src/test/resources/fixtures/mov_h265.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..98b1e129387b5d0e57ed5058e02b398641412caf Binary files /dev/null and b/io/src/test/resources/fixtures/mov_h265.mp4 differ