diff --git a/README.md b/README.md index ead0b57..3bae18a 100644 --- a/README.md +++ b/README.md @@ -105,12 +105,17 @@ with the AWS SDK for the Kinesis Video. This example provides examples for ## Release Notes +### Release 1.0.6 (Sep 2018) +* Introduce handling for empty fragment metadata +* Added a new SimpleFrame Visitor to handle video with no tags +* Refactored H264FrameDecoder, so that base class method can be reused by child class + ### Release 1.0.5 (May 2018) -* Introduce `GetMediaResponseStreamConsumer` as an abstract class used to consume the output of a GetMedia* call +* Introduce `GetMediaResponseStreamConsumer` as an abstract class used to consume the output of a GetMedia* call to Kinesis Video in a streaming fashion. Child classes will use visitors to implement different consumers. * The `MergedOutputPiper` extends `GetMediaResponseStreamConsumer` to merge consecutive mkv streams in the output of GetMedia - and pipes the merged stream to the stdin of a child process. -* Add the capability and example to pipe the output of GetMedia calls to GStreamer using `MergedOutputPiper`. + and pipes the merged stream to the stdin of a child process. +* Add the capability and example to pipe the output of GetMedia calls to GStreamer using `MergedOutputPiper`. ### Release 1.0.4 (April 2018) * Add example for KinesisVideo Streams integration with Rekognition and draw Bounding Boxes for every sampled frame. diff --git a/pom.xml b/pom.xml index ebe0ea0..9485a95 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ amazon-kinesis-video-streams-parser-library jar Amazon Kinesis Video Streams Parser Library - 1.0.5-SNAPSHOT + 1.0.6 The Amazon Kinesis Video Streams Parser Library for Java enables Java developers to parse the streams returned by GetMedia calls to Amazon Kinesis Video. @@ -107,6 +107,13 @@ 2.8.1 test + + org.powermock + powermock-mockito-release-full + 1.6.3 + pom + test + diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadata.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadata.java index 79c7d98..19a5823 100644 --- a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadata.java +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadata.java @@ -94,11 +94,12 @@ static FragmentMetadata createFromtagNametoValueMap(final Map ta return new FragmentMetadata(getValueForTag(tagNameToTagValueMap, FRAGMENT_NUMBER_KEY), Double.parseDouble(getValueForTag(tagNameToTagValueMap, SERVER_SIDE_TIMESTAMP_KEY)), Double.parseDouble(getValueForTag(tagNameToTagValueMap, PRODCUER_SIDE_TIMESTAMP_KEY))); - } else { + } else if (tagNameToTagValueMap.containsKey(FRAGMENT_NUMBER_KEY)) { return new FragmentMetadata(getValueForTag(tagNameToTagValueMap, FRAGMENT_NUMBER_KEY), Long.parseLong(getValueForTag(tagNameToTagValueMap, ERROR_ID_KEY)), getValueForTag(tagNameToTagValueMap, ERROR_CODE_KEY)); } + return null; } private static String getValueForTag(Map tagNameToTagValueMap, String tagName) { diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitor.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitor.java index 00f060e..49e409a 100644 --- a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitor.java +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitor.java @@ -207,7 +207,7 @@ private void setMillisBehindLatestAndContinuationToken() { private void collectPreClusterInfo() { final Map tagNameToTagValueMap = getTagNameToValueMap(); - currentFragmentMetadata = Optional.of(FragmentMetadata.createFromtagNametoValueMap(tagNameToTagValueMap)); + currentFragmentMetadata = Optional.ofNullable(FragmentMetadata.createFromtagNametoValueMap(tagNameToTagValueMap)); final Map> trackEntryElementNumberToMkvElement = getTrackEntryMap(); trackEntryElementNumberToMkvElement.values().stream().forEach(this::createTrackMetadata); diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitor.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitor.java index f47dcd0..52ba05c 100644 --- a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitor.java +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitor.java @@ -74,4 +74,4 @@ public void visit(com.amazonaws.kinesisvideo.parser.mkv.MkvDataElement dataEleme } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoder.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoder.java new file mode 100644 index 0000000..7e04e9f --- /dev/null +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoder.java @@ -0,0 +1,90 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.mkv.Frame; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jcodec.codecs.h264.H264Decoder; +import org.jcodec.codecs.h264.mp4.AvcCBox; +import org.jcodec.common.model.ColorSpace; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.jcodec.scale.Transform; +import org.jcodec.scale.Yuv420jToRgb; + +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Optional; + +import static org.jcodec.codecs.h264.H264Utils.splitMOVPacket; + +@Slf4j +public class H264FrameDecoder implements FrameVisitor.FrameProcessor { + + private final H264Decoder decoder = new H264Decoder(); + private final Transform transform = new Yuv420jToRgb(); + + @Getter + private int frameCount; + + private byte[] codecPrivateData; + + @Override + public void process(Frame frame, MkvTrackMetadata trackMetadata, Optional fragmentMetadata) { + decodeH264Frame(frame, trackMetadata); + } + + protected BufferedImage decodeH264Frame(Frame frame, MkvTrackMetadata trackMetadata) { + ByteBuffer frameBuffer = frame.getFrameData(); + int pixelWidth = trackMetadata.getPixelWidth().get().intValue(); + int pixelHeight = trackMetadata.getPixelHeight().get().intValue(); + codecPrivateData = trackMetadata.getCodecPrivateData().array(); + log.debug("Decoding frames ... "); + // Read the bytes that appear to comprise the header + // See: https://www.matroska.org/technical/specs/index.html#simpleblock_structure + + Picture rgb = Picture.create(pixelWidth, pixelHeight, ColorSpace.RGB); + BufferedImage bufferedImage = new BufferedImage(pixelWidth, pixelHeight, BufferedImage.TYPE_3BYTE_BGR); + AvcCBox avcC = AvcCBox.parseAvcCBox(ByteBuffer.wrap(codecPrivateData)); + + decoder.addSps(avcC.getSpsList()); + decoder.addPps(avcC.getPpsList()); + + Picture buf = Picture.create(pixelWidth, pixelHeight, ColorSpace.YUV420J); + List byteBuffers = splitMOVPacket(frameBuffer, avcC); + Picture pic = decoder.decodeFrameFromNals(byteBuffers, buf.getData()); + + if (pic != null) { + // Work around for color issues in JCodec + // https://github.com/jcodec/jcodec/issues/59 + // https://github.com/jcodec/jcodec/issues/192 + byte[][] dataTemp = new byte[3][pic.getData().length]; + dataTemp[0] = pic.getPlaneData(0); + dataTemp[1] = pic.getPlaneData(2); + dataTemp[2] = pic.getPlaneData(1); + + Picture tmpBuf = Picture.createPicture(pixelWidth, pixelHeight, dataTemp, ColorSpace.YUV420J); + transform.transform(tmpBuf, rgb); + AWTUtil.toBufferedImage(rgb, bufferedImage); + frameCount++; + } + return bufferedImage; + } + + public ByteBuffer getCodecPrivateData() { + return ByteBuffer.wrap(codecPrivateData); + } +} diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRenderer.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRenderer.java index c8cec85..819da70 100644 --- a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRenderer.java +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRenderer.java @@ -33,18 +33,12 @@ import static org.jcodec.codecs.h264.H264Utils.splitMOVPacket; @Slf4j -public class H264FrameRenderer implements FrameVisitor.FrameProcessor { +public class H264FrameRenderer extends H264FrameDecoder { private final KinesisVideoFrameViewer kinesisVideoFrameViewer; - private final H264Decoder decoder = new H264Decoder(); - private final Transform transform = new Yuv420jToRgb(); - - @Getter - private int frameCount; - - private byte[] codecPrivateData; protected H264FrameRenderer(final KinesisVideoFrameViewer kinesisVideoFrameViewer) { + super(); this.kinesisVideoFrameViewer = kinesisVideoFrameViewer; this.kinesisVideoFrameViewer.setVisible(true); } @@ -58,46 +52,4 @@ public void process(Frame frame, MkvTrackMetadata trackMetadata, Optional byteBuffers = splitMOVPacket(frameBuffer, avcC); - Picture pic = decoder.decodeFrameFromNals(byteBuffers, buf.getData()); - - if (pic != null) { - // Work around for color issues in JCodec - // https://github.com/jcodec/jcodec/issues/59 - // https://github.com/jcodec/jcodec/issues/192 - byte[][] dataTemp = new byte[3][pic.getData().length]; - dataTemp[0] = pic.getPlaneData(0); - dataTemp[1] = pic.getPlaneData(2); - dataTemp[2] = pic.getPlaneData(1); - - Picture tmpBuf = Picture.createPicture(pixelWidth, pixelHeight, dataTemp, ColorSpace.YUV420J); - transform.transform(tmpBuf, rgb); - AWTUtil.toBufferedImage(rgb, bufferedImage); - frameCount++; - } - return bufferedImage; - } - - public ByteBuffer getCodecPrivateData() { - return ByteBuffer.wrap(codecPrivateData); - } - } diff --git a/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitor.java b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitor.java new file mode 100644 index 0000000..90c96f0 --- /dev/null +++ b/src/main/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitor.java @@ -0,0 +1,91 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.ebml.MkvTypeInfos; +import com.amazonaws.kinesisvideo.parser.mkv.Frame; +import com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException; +import com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitor; +import com.amazonaws.kinesisvideo.parser.mkv.MkvValue; +import com.amazonaws.kinesisvideo.parser.mkv.visitors.CompositeMkvElementVisitor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.Validate; + +import java.math.BigInteger; + +/** + * Fragment metdata tags will not be present as there is no tag when the data (mkv/webm) is not + * retrieved from Kinesis video. + * As a result, user cannot get the timestamp from fragment metadata tags. + * This class is used to provide a FrameVisitor to process frame as well as time elements for timestamp. + **/ + +@Slf4j +public class SimpleFrameVisitor extends CompositeMkvElementVisitor { + private final FrameVisitorInternal frameVisitorInternal; + private final FrameProcessor frameProcessor; + + private SimpleFrameVisitor(FrameProcessor frameProcessor) { + this.frameProcessor = frameProcessor; + frameVisitorInternal = new FrameVisitorInternal(); + childVisitors.add(frameVisitorInternal); + + } + public static SimpleFrameVisitor create(FrameProcessor frameProcessor) { + return new SimpleFrameVisitor(frameProcessor); + } + public interface FrameProcessor { + default void process(Frame frame, long clusterTimeCode, long timeCodeScale) { + throw new NotImplementedException("Default FrameVisitor with No Fragement MetaData"); + } + } + + private class FrameVisitorInternal extends MkvElementVisitor { + private long clusterTimeCode = -1; + private long timeCodeScale = -1; + @Override + public void visit(com.amazonaws.kinesisvideo.parser.mkv.MkvStartMasterElement startMasterElement) + throws com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException { + } + + @Override + public void visit(com.amazonaws.kinesisvideo.parser.mkv.MkvEndMasterElement endMasterElement) + throws com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException { + } + + @Override + public void visit(com.amazonaws.kinesisvideo.parser.mkv.MkvDataElement dataElement) + throws com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException { + + if (MkvTypeInfos.TIMECODE.equals(dataElement.getElementMetaData().getTypeInfo())) { + clusterTimeCode = ((BigInteger) dataElement.getValueCopy().getVal()).longValue(); + } + + if (MkvTypeInfos.TIMECODESCALE.equals(dataElement.getElementMetaData().getTypeInfo())) { + timeCodeScale = ((BigInteger) dataElement.getValueCopy().getVal()).longValue(); + } + + if (MkvTypeInfos.SIMPLEBLOCK.equals(dataElement.getElementMetaData().getTypeInfo())) { + if (clusterTimeCode == -1 || timeCodeScale == -1) { + throw new MkvElementVisitException("No timeCodeScale or timeCode found", new RuntimeException()); + } + final MkvValue frame = dataElement.getValueCopy(); + Validate.notNull(frame); + frameProcessor.process(frame.getVal(), clusterTimeCode, timeCodeScale); + } + } + + } +} diff --git a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitorTest.java b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitorTest.java index ac05fe9..3c14e4e 100644 --- a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitorTest.java +++ b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FragmentMetadataVisitorTest.java @@ -51,7 +51,7 @@ public void basicTest() throws IOException, MkvElementVisitException { continuationTokens.add("91343852333181432397633822764885441725874549018"); continuationTokens.add("91343852333181432402585582922026963247510532162"); - FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); + final FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); StreamingMkvReader mkvStreamReader = StreamingMkvReader.createDefault(new InputStreamParserByteSource(in)); int segmentCount = 0; @@ -102,7 +102,7 @@ private void assertTrackAndFragmentInfo(FragmentMetadataVisitor fragmentVisitor, @Test public void withOutputSegmentMergerTest() throws IOException, MkvElementVisitException { - FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); + final FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -133,7 +133,7 @@ public void withOutputSegmentMergerTest() throws IOException, MkvElementVisitExc @Test public void testFragmentNumbers_NoClusterData() throws IOException, MkvElementVisitException { - FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); + final FragmentMetadataVisitor fragmentVisitor = FragmentMetadataVisitor.create(); String testFile = "empty-mkv-with-tags.mkv"; Set expectedFragmentNumbers = new HashSet<>( Arrays.asList( @@ -159,6 +159,46 @@ public void testFragmentNumbers_NoClusterData() throws IOException, MkvElementVi Assert.assertEquals(expectedFragmentNumbers, visitedFragmentNumbers); } + @Test + public void testFragmentMetadata_NoFragementMetadata_withWebm() throws IOException, MkvElementVisitException { + final FragmentMetadataVisitor fragmentMetadataVisitor = FragmentMetadataVisitor.create(); + final String testFile = "big-buck-bunny_trailer.webm"; + int metadataCount = 0; + final StreamingMkvReader mkvStreamReader = StreamingMkvReader + .createDefault(new InputStreamParserByteSource(TestResourceUtil.getTestInputStream(testFile))); + while (mkvStreamReader.mightHaveNext()) { + Optional mkvElement = mkvStreamReader.nextIfAvailable(); + if (mkvElement.isPresent()) { + mkvElement.get().accept(fragmentMetadataVisitor); + Optional fragmentMetadata = fragmentMetadataVisitor.getCurrentFragmentMetadata(); + if(fragmentMetadata.isPresent()) { + metadataCount ++; + } + } + } + Assert.assertEquals(0, metadataCount); + } + + @Test + public void testFragmentMetadata_NoFragementMetadata_withMkv() throws IOException, MkvElementVisitException { + final FragmentMetadataVisitor fragmentMetadataVisitor = FragmentMetadataVisitor.create(); + final String testFile = "clusters.mkv"; + int metadataCount = 0; + final StreamingMkvReader mkvStreamReader = StreamingMkvReader + .createDefault(new InputStreamParserByteSource(TestResourceUtil.getTestInputStream(testFile))); + while (mkvStreamReader.mightHaveNext()) { + Optional mkvElement = mkvStreamReader.nextIfAvailable(); + if (mkvElement.isPresent()) { + mkvElement.get().accept(fragmentMetadataVisitor); + Optional fragmentMetadata = fragmentMetadataVisitor.getCurrentFragmentMetadata(); + if(fragmentMetadata.isPresent()) { + metadataCount ++; + } + } + } + Assert.assertEquals(0, metadataCount); + } + private static class TestCompositeVisitor extends CompositeMkvElementVisitor { public TestCompositeVisitor(FragmentMetadataVisitor fragmentMetadataVisitor, OutputSegmentMerger merger) { super(fragmentMetadataVisitor, merger); diff --git a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitorTest.java b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitorTest.java new file mode 100644 index 0000000..79aa8cf --- /dev/null +++ b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/FrameVisitorTest.java @@ -0,0 +1,71 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.TestResourceUtil; +import com.amazonaws.kinesisvideo.parser.ebml.InputStreamParserByteSource; + +import com.amazonaws.kinesisvideo.parser.mkv.Frame; +import com.amazonaws.kinesisvideo.parser.mkv.StreamingMkvReader; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +public class FrameVisitorTest { + private int frameProcessCount; + private int nullFrameCount; + private static int SIMPLE_BLOCKS_COUNT_MKV = 444; + + private final class TestFrameProcessor implements FrameVisitor.FrameProcessor { + @Override + public void process(final Frame frame, final MkvTrackMetadata trackMetadata, + final Optional fragmentMetadata) { + frameProcessCount++; + if(frame == null) { + nullFrameCount++; + } + } + + } + private FrameVisitor frameVisitor; + private StreamingMkvReader streamingMkvReader; + + @Before + public void setUp() throws Exception { + frameVisitor = FrameVisitor.create(new TestFrameProcessor()); + } + + @Test + public void testWithMkvVideo() throws Exception { + streamingMkvReader = StreamingMkvReader.createDefault(getClustersByteSource("clusters.mkv")); + streamingMkvReader.apply(frameVisitor); + Assert.assertEquals(SIMPLE_BLOCKS_COUNT_MKV, frameProcessCount); + Assert.assertEquals(0, nullFrameCount); + } + + private InputStreamParserByteSource getClustersByteSource(String name) throws IOException { + return getInputStreamParserByteSource(name); + } + + private InputStreamParserByteSource getInputStreamParserByteSource(String fileName) throws IOException { + final InputStream in = TestResourceUtil.getTestInputStream(fileName); + return new InputStreamParserByteSource(in); + } + +} diff --git a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoderTest.java b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoderTest.java new file mode 100644 index 0000000..bf4ed8c --- /dev/null +++ b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameDecoderTest.java @@ -0,0 +1,59 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.TestResourceUtil; +import com.amazonaws.kinesisvideo.parser.ebml.InputStreamParserByteSource; +import com.amazonaws.kinesisvideo.parser.ebml.MkvTypeInfos; +import com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException; +import com.amazonaws.kinesisvideo.parser.mkv.StreamingMkvReader; +import com.amazonaws.kinesisvideo.parser.mkv.visitors.CompositeMkvElementVisitor; +import com.amazonaws.kinesisvideo.parser.mkv.visitors.CountVisitor; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class H264FrameDecoderTest { + + @Test + public void frameDecodeCountTest() throws IOException, MkvElementVisitException { + final InputStream in = TestResourceUtil.getTestInputStream("kinesis_video_renderer_example_output.mkv"); + byte[] codecPrivateData = new byte[]{ + 0x01, 0x64, 0x00, 0x28, (byte) 0xff, (byte) 0xe1, 0x00, + 0x0e, 0x27, 0x64, 0x00, 0x28, (byte) 0xac, 0x2b, 0x40, + 0x50, 0x1e, (byte) 0xd0, 0x0f, 0x12, 0x26, (byte) 0xa0, + 0x01, 0x00, 0x04, 0x28, (byte) 0xee, 0x1f, 0x2c + }; + + H264FrameDecoder frameDecoder = new H264FrameDecoder(); + StreamingMkvReader mkvStreamReader = + StreamingMkvReader.createDefault(new InputStreamParserByteSource(in)); + + CountVisitor countVisitor = CountVisitor.create(MkvTypeInfos.CLUSTER, MkvTypeInfos.SEGMENT, + MkvTypeInfos.SIMPLEBLOCK, MkvTypeInfos.TRACKS); + + mkvStreamReader.apply(new CompositeMkvElementVisitor(countVisitor, FrameVisitor.create(frameDecoder))); + + Assert.assertEquals(8, countVisitor.getCount(MkvTypeInfos.TRACKS)); + Assert.assertEquals(8, countVisitor.getCount(MkvTypeInfos.CLUSTER)); + Assert.assertEquals(444, countVisitor.getCount(MkvTypeInfos.SIMPLEBLOCK)); + + Assert.assertEquals(444, frameDecoder.getFrameCount()); + ByteBuffer codecPrivateDataFromFrame = frameDecoder.getCodecPrivateData(); + Assert.assertEquals(ByteBuffer.wrap(codecPrivateData), codecPrivateDataFromFrame); + } +} diff --git a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRendererTest.java b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRendererTest.java new file mode 100644 index 0000000..f3a348d --- /dev/null +++ b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/H264FrameRendererTest.java @@ -0,0 +1,61 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.TestResourceUtil; +import com.amazonaws.kinesisvideo.parser.ebml.InputStreamParserByteSource; +import com.amazonaws.kinesisvideo.parser.ebml.MkvTypeInfos; +import com.amazonaws.kinesisvideo.parser.examples.KinesisVideoFrameViewer; +import com.amazonaws.kinesisvideo.parser.mkv.*; +import com.amazonaws.kinesisvideo.parser.mkv.visitors.CompositeMkvElementVisitor; +import com.amazonaws.kinesisvideo.parser.mkv.visitors.CountVisitor; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; + + +@RunWith(PowerMockRunner.class) +public class H264FrameRendererTest { + + @Test + public void frameRenderCountTest() throws IOException, MkvElementVisitException { + final InputStream in = TestResourceUtil.getTestInputStream("kinesis_video_renderer_example_output.mkv"); + + final KinesisVideoFrameViewer kinesisVideoFrameViewer = mock(KinesisVideoFrameViewer.class); + doNothing().when(kinesisVideoFrameViewer).update(any(BufferedImage.class)); + H264FrameRenderer frameRenderer = H264FrameRenderer.create(kinesisVideoFrameViewer); + StreamingMkvReader mkvStreamReader = + StreamingMkvReader.createDefault(new InputStreamParserByteSource(in)); + + CountVisitor countVisitor = + CountVisitor.create(MkvTypeInfos.CLUSTER, MkvTypeInfos.SEGMENT, MkvTypeInfos.SIMPLEBLOCK); + + mkvStreamReader.apply(new CompositeMkvElementVisitor(countVisitor, FrameVisitor.create(frameRenderer))); + + Assert.assertEquals(444, frameRenderer.getFrameCount()); + + verify(kinesisVideoFrameViewer, times(444)).update(any(BufferedImage.class)); + } +} diff --git a/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitorTest.java b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitorTest.java new file mode 100644 index 0000000..efbdf06 --- /dev/null +++ b/src/test/java/com/amazonaws/kinesisvideo/parser/utilities/SimpleFrameVisitorTest.java @@ -0,0 +1,128 @@ +/* +Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. +This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. +*/ +package com.amazonaws.kinesisvideo.parser.utilities; + +import com.amazonaws.kinesisvideo.parser.TestResourceUtil; +import com.amazonaws.kinesisvideo.parser.ebml.EBMLElementMetaData; +import com.amazonaws.kinesisvideo.parser.ebml.InputStreamParserByteSource; +import com.amazonaws.kinesisvideo.parser.ebml.MkvTypeInfos; +import com.amazonaws.kinesisvideo.parser.mkv.Frame; +import com.amazonaws.kinesisvideo.parser.mkv.MkvDataElement; +import com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitException; +import com.amazonaws.kinesisvideo.parser.mkv.MkvElementVisitor; +import com.amazonaws.kinesisvideo.parser.mkv.StreamingMkvReader; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.io.IOException; +import java.io.InputStream; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({SimpleFrameVisitor.class}) +public class SimpleFrameVisitorTest { + private int frameProcessCount; + private int nullFrameCount; + private static int SIMPLE_BLOCKS_COUNT_WEBM = 2308; + private static int SIMPLE_BLOCKS_COUNT_MKV = 444; + + private final class TestFrameProcessor implements SimpleFrameVisitor.FrameProcessor { + @Override + public void process(final Frame frame, long clusterTimeCode, long timeCodeScale) { + frameProcessCount++; + if(frame == null) { nullFrameCount++; } + } + } + + @Mock + private MkvDataElement mockDataElement; + @Mock + private EBMLElementMetaData mockElementMetaData; + private SimpleFrameVisitor frameVisitor; + private StreamingMkvReader streamingMkvReader; + + @Before + public void setUp() throws Exception { + frameVisitor = SimpleFrameVisitor + .create(new TestFrameProcessor()); + } + + @Test + public void testWithWebmVideo() throws Exception { + streamingMkvReader = StreamingMkvReader + .createDefault(getClustersByteSource("big-buck-bunny_trailer.webm")); + streamingMkvReader.apply(frameVisitor); + + Assert.assertEquals(SIMPLE_BLOCKS_COUNT_WEBM, frameProcessCount); + Assert.assertEquals(nullFrameCount, 0); + MkvElementVisitor internal = Whitebox.getInternalState(frameVisitor, "frameVisitorInternal"); + long timeCode = Whitebox.getInternalState(internal, "clusterTimeCode"); + long timeCodeScale = Whitebox.getInternalState(internal, "timeCodeScale"); + Assert.assertNotEquals(-1, timeCode); + Assert.assertNotEquals(-1, timeCodeScale); + + } + + @Test + public void testWithMkvVideo() throws Exception { + streamingMkvReader = StreamingMkvReader.createDefault(getClustersByteSource("clusters.mkv")); + streamingMkvReader.apply(frameVisitor); + + Assert.assertEquals(SIMPLE_BLOCKS_COUNT_MKV, frameProcessCount); + Assert.assertEquals(nullFrameCount, 0); + MkvElementVisitor internal = Whitebox.getInternalState(frameVisitor, "frameVisitorInternal"); + long timeCode = Whitebox.getInternalState(internal, "clusterTimeCode"); + long timeCodeScale = Whitebox.getInternalState(internal, "timeCodeScale"); + Assert.assertNotEquals(-1, timeCode); + Assert.assertNotEquals(-1, timeCodeScale); + } + + @Test(expected = MkvElementVisitException.class) + public void testWhenNoTimeCode() throws Exception { + MkvElementVisitor internal = Whitebox.getInternalState(frameVisitor, "frameVisitorInternal"); + Whitebox.setInternalState(internal, "timeCodeScale", 1); + PowerMockito.when(mockDataElement.getElementMetaData()).thenReturn(mockElementMetaData); + PowerMockito.when(mockElementMetaData.getTypeInfo()).thenReturn(MkvTypeInfos.SIMPLEBLOCK); + internal.visit(mockDataElement); + + } + + + @Test(expected = MkvElementVisitException.class) + public void testWhenNoTimeCodeScale() throws Exception { + MkvElementVisitor internal = Whitebox.getInternalState(frameVisitor, "frameVisitorInternal"); + Whitebox.setInternalState(internal, "clusterTimeCode", 1); + PowerMockito.when(mockDataElement.getElementMetaData()).thenReturn(mockElementMetaData); + PowerMockito.when(mockElementMetaData.getTypeInfo()).thenReturn(MkvTypeInfos.SIMPLEBLOCK); + internal.visit(mockDataElement); + + } + + private InputStreamParserByteSource getClustersByteSource(String name) throws IOException { + return getInputStreamParserByteSource(name); + } + + private InputStreamParserByteSource getInputStreamParserByteSource(String fileName) throws IOException { + final InputStream in = TestResourceUtil.getTestInputStream(fileName); + return new InputStreamParserByteSource(in); + } + +}