diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/AbstractMediaSourceConfiguration.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/AbstractMediaSourceConfiguration.java new file mode 100644 index 0000000000..7708784258 --- /dev/null +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/AbstractMediaSourceConfiguration.java @@ -0,0 +1,404 @@ +/** + * Copyright 2017-2018 Amazon.com, + * Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Amazon Software License (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/asl/ + * + * or in the "license" file accompanying this file. This file is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, express or implied. See the License + * for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.kinesisvideo.client.mediasource; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceConfiguration; +import com.amazonaws.kinesisvideo.producer.StreamInfo; + +/** + * This abstract class defines common configuration properties for media sources. + */ +public abstract class AbstractMediaSourceConfiguration implements MediaSourceConfiguration { + /** + * Refernce to the builder used for creating this configuration object. + */ + protected final Builder mBuilder; + + /** + * Generic abstract builder for this configuration class. + * It uses recursive generics pattern to be able to preserve the chaining capability of builders. + * @param MediaSource type. + * @param Builder type. + */ + public static abstract class Builder> implements MediaSourceConfiguration.Builder { + + protected String mMimeType; + protected int mFrameRate; + protected int mHorizontalResolution; + protected int mVerticalResolution; + protected String mOutputFileName; + protected int mEncodingBitrate; + protected boolean mIsEncoderHardwareAccelerated; + protected int mGopDurationMillis; + protected byte[] mCodecPrivateData; + protected long mFrameTimescale; + protected StreamInfo.NalAdaptationFlags mNalAdaptationFlags; + protected boolean mIsAbsoluteTimecode; + protected int mRetentionPeriodInHours; + + /** + * Set the MIME type. + * @param mimeType Input value. + * @return Reference to the same builder object and type. + */ + public S withEncodingMimeType(final String mimeType) { + mMimeType = mimeType; + return (S)this; + } + + /** + * Set the retention period in hours. + * @param retentionPeriodInHours Input value. + * @return Reference to the same builder object and type. + */ + public S withRetentionPeriodInHours(final int retentionPeriodInHours) { + mRetentionPeriodInHours = retentionPeriodInHours; + return (S)this; + } + + /** + * Set the frame rate. + * @param frameRate Input value. + * @return Reference to the same builder object and type. + */ + public S withFrameRate(final int frameRate) { + mFrameRate = frameRate; + return (S)this; + } + + /** + * Set the output file name. + * @param outputFileName Input value. + * @return Reference to the same builder object and type. + */ + public S withFileOutput(final String outputFileName) { + mOutputFileName = outputFileName; + return (S)this; + } + + /** + * Set the horizontal resolution. + * @param horizontalResolution Input value. + * @return Reference to the same builder object and type. + */ + public S withHorizontalResolution(final int horizontalResolution) { + mHorizontalResolution = horizontalResolution; + return (S)this; + } + + /** + * Set the vertical resolution. + * @param verticalResolution Input value. + * @return Reference to the same builder object and type. + */ + public S withVerticalResolution(final int verticalResolution) { + mVerticalResolution = verticalResolution; + return (S)this; + } + + /** + * Set the encoding bit rate. + * @param bitrate Input value. + * @return Reference to the same builder object and type. + */ + public S withEncodingBitRate(final int bitrate) { + mEncodingBitrate = bitrate; + return (S)this; + } + + /** + * Set the hardware acceleration flag. + * @param isAccelerated Input value. + * @return Reference to the same builder object and type. + */ + public S withIsEncoderHardwareAccelerated(final boolean isAccelerated) { + mIsEncoderHardwareAccelerated = isAccelerated; + return (S)this; + } + + /** + * Set the codec private data. + * @param privateData Input value. + * @return Reference to the same builder object and type. + */ + public S withCodecPrivateData(final byte[] privateData) { + mCodecPrivateData = privateData; + return (S)this; + } + + /** + * Set the frame time scale. + * @param timescale Input value. + * @return Reference to the same builder object and type. + */ + public S withFrameTimeScale(final long timescale) { + mFrameTimescale = timescale; + return (S)this; + } + + /** + * Set the GOP duration in millis. + * @param gopDuration Input value. + * @return Reference to the same builder object and type. + */ + public S withGopDurationMillis(final int gopDuration) { + mGopDurationMillis = gopDuration; + return (S)this; + } + + /** + * Set the NAL adaptation flags. + * @param nalAdaptationFlags Input value. + * @return Reference to the same builder object and type. + */ + public S withNalAdaptationFlags(final StreamInfo.NalAdaptationFlags nalAdaptationFlags) { + mNalAdaptationFlags = nalAdaptationFlags; + return (S)this; + } + + /** + * Set the flag for absolute time code. + * @param isAbsoluteTimecode Input value. + * @return Reference to the same builder object and type. + */ + public S withIsAbsoluteTimecode(final boolean isAbsoluteTimecode) { + mIsAbsoluteTimecode = isAbsoluteTimecode; + return (S)this; + } + + /** + * Get the MIME type. + * @return MIME type string. + */ + public String getMimeType() { + return mMimeType; + } + + /** + * Get the frame rate. + * @return Frame rate integer. + */ + public int getFrameRate() { + return mFrameRate; + } + + /** + * Gets the retention period in hours. + * @return Retention period as integer. + */ + public int getmRetentionPeriodInHours() { + return mRetentionPeriodInHours; + } + + /** + * Get the horizontal resolution. + * @return Horizontal resolution as an integer. + */ + public int getHorizontalResolution() { + return mHorizontalResolution; + } + + /** + * Get the vertical resolution. + * @return Vertical resolution as an integer. + */ + public int getVerticalResolution() { + return mVerticalResolution; + } + + /** + * Get the output file name. + * @return Output file name string. + */ + public String getOutputFileName() { + return mOutputFileName; + } + + /** + * Get the encoding bit rate. + * @return Encoding bit rate as integer. + */ + public int getEncodingBitrate() { + return mEncodingBitrate; + } + + /** + * Check if hardware acceleration is turned on. + * @return True if hardware acceleration is turned on + */ + public boolean isEncoderHardwareAccelerated() { + return mIsEncoderHardwareAccelerated; + } + + /** + * Get GOP duration. + * @return GOP duratio in millis as integer. + */ + public int getGopDurationMillis() { + return mGopDurationMillis; + } + + /** + * Get codec private data. + * @return Codec private data. + */ + public byte[] getCodecPrivateData() { + return mCodecPrivateData; + } + + /** + * Get the frame time scale. + * @return Frame time scale as a long value. + */ + public long getFrameTimescale() { + return mFrameTimescale; + } + + /** + * Get NAL adaptation flags. + * @return NAL adaptation flags. + */ + public StreamInfo.NalAdaptationFlags getNalAdaptationFlags() { + return mNalAdaptationFlags; + } + + /** + * Abstract method to build the MediaSourceConfiguration using this builder. + * @return Instance of MediaSourceConfiguration of the generic type. + */ + public abstract T build(); + } + + /** + * Constructor accepting a builder. + * @param builder The builder from which the configuration is to be obtained. + */ + public AbstractMediaSourceConfiguration(final Builder builder) { + mBuilder = builder; + } + + /** + * Get the horizontal resolution. + * @return Horizontal resolution as an integer. + */ + public int getHorizontalResolution() { + return mBuilder.mHorizontalResolution; + } + + /** + * Get the vertical resolution. + * @return Vertical resolution as an integer. + */ + public int getVerticalResolution() { + return mBuilder.mVerticalResolution; + } + + /** + * Get the output file name. + * @return Output file name string. + */ + public String getOutputFileName() { + return mBuilder.mOutputFileName; + } + + /** + * Get the frame rate. + * @return Frame rate integer. + */ + public int getFrameRate() { + return mBuilder.mFrameRate; + } + + /** + * Gets the retention period in hours. + * @return Retention period as integer. + */ + public int getRetentionPeriodInHours() { + return mBuilder.mRetentionPeriodInHours; + } + + /** + * Get the encoding bit rate. + * @return Encoding bit rate as integer. + */ + public int getBitRate() { + return mBuilder.mEncodingBitrate; + } + + /** + * Get the MIME type. + * @return MIME type string. + */ + @NonNull + public String getEncoderMimeType() { + return mBuilder.mMimeType; + } + + /** + * Get GOP duration. + * @return GOP duratio in millis as integer. + */ + public int getGopDurationMillis() { + return mBuilder.mGopDurationMillis; + } + + /** + * Check if hardware acceleration is turned on. + * @return True if hardware acceleration is turned on + */ + public boolean isEndcoderHardwareAccelerated() { + return mBuilder.mIsEncoderHardwareAccelerated; + } + + /** + * Get codec private data. + * @return Codec private data. + */ + @Nullable + public byte[] getCodecPrivateData() { + return mBuilder.mCodecPrivateData; + } + + /** + * Get the frame time scale. + * @return Frame time scale as a long value. + */ + public long getTimeScale() { + return mBuilder.mFrameTimescale; + } + + /** + * Get NAL adaptation flags. + * @return NAL adaptation flags. + */ + public StreamInfo.NalAdaptationFlags getNalAdaptationFlags() { + return mBuilder.mNalAdaptationFlags; + } + + /** + * Get if timecode is absolute or not. + * @return True of the timecode is absolute. + */ + public boolean getIsAbsoluteTimecode() { + return mBuilder.mIsAbsoluteTimecode; + } +} diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/CameraMediaSourceConfiguration.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/CameraMediaSourceConfiguration.java index f4a2ce4301..3f7e6ae7f9 100644 --- a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/CameraMediaSourceConfiguration.java +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/kinesisvideo/client/mediasource/CameraMediaSourceConfiguration.java @@ -23,314 +23,154 @@ import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceConfiguration; import com.amazonaws.kinesisvideo.producer.StreamInfo; -//import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** - * CameraMediaSourceConfiguration defines common configuration properties for camera media source + * CameraMediaSourceConfiguration defines configuration properties for camera media sources. */ -//@SuppressFBWarnings({"EI_EXPOSE_REP", "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD"}) -public class CameraMediaSourceConfiguration implements MediaSourceConfiguration { +public class CameraMediaSourceConfiguration extends AbstractMediaSourceConfiguration { + /** + * Description string for this media source configuration class. + */ private static final String MEDIA_SOURCE_DESCRIPTION = "Configuration for a camera media source"; + + /** + * Unique type string for this media source configuration class. + */ public static final String MEDIA_SOURCE_TYPE = "AbstractCameraMediaSource"; - public static class Builder implements MediaSourceConfiguration.Builder { + /** + * Generic concrete builder for this configuration class. + * It uses recursive generics pattern to be able to preserve the chaining capability of builders. + * @param MediaSource type. + * @param Builder type. + */ + public static class Builder> extends AbstractMediaSourceConfiguration.Builder { - private String mMimeType; - private int mFrameRate; - private int mHorizontalResolution; - private int mVerticalResolution; - private String mOutputFileName; private String mCameraId; private int mCameraFacing; private int mCameraOrientation; - private int mEncodingBitrate; - private boolean mIsEncoderHardwareAccelerated; - private int mGopDurationMillis; - private byte[] mCodecPrivateData; - private long mFrameTimescale; - private StreamInfo.NalAdaptationFlags mNalAdaptationFlags; - private boolean mIsAbsoluteTimecode; - private int mRetentionPeriodInHours; - - public Builder withEncodingMimeType(final String mimeType) { - mMimeType = mimeType; - return this; - } - - public Builder withRetentionPeriodInHours(final int retentionPeriodInHours) { - mRetentionPeriodInHours = retentionPeriodInHours; - return this; - } - - public Builder withFrameRate(final int frameRate) { - mFrameRate = frameRate; - return this; - } - - public Builder withFileOutput(final String outputFileName) { - mOutputFileName = outputFileName; - return this; - } - public Builder withCameraId(final String cameraId) { + /** + * Set the camera ID. + * @param mimeType Input value. + * @return Reference to the same builder object and type. + */ + public S withCameraId(final String cameraId) { mCameraId = cameraId; - return this; + return (S)this; } - public Builder withHorizontalResolution(final int horizontalResolution) { - mHorizontalResolution = horizontalResolution; - return this; - } - - public Builder withVerticalResolution(final int verticalResolution) { - mVerticalResolution = verticalResolution; - return this; - } - - public Builder withCameraFacing(final int facing) { + /** + * Set the camera facing. + * @param facing Input value. + * @return Reference to the same builder object and type. + */ + public S withCameraFacing(final int facing) { mCameraFacing = facing; - return this; - } - - public Builder withCameraOrientation(final int orientation) { - mCameraOrientation = orientation; - return this; - } - - public Builder withEncodingBitRate(final int bitrate) { - mEncodingBitrate = bitrate; - return this; - } - - public Builder withIsEncoderHardwareAccelerated(final boolean isAccelerated) { - mIsEncoderHardwareAccelerated = isAccelerated; - return this; - } - - public Builder withCodecPrivateData(final byte[] privateData) { - mCodecPrivateData = privateData; - return this; - } - - public Builder withFrameTimeScale(final long timescale) { - mFrameTimescale = timescale; - return this; + return (S)this; } - public Builder withGopDurationMillis(final int gopDuration) { - mGopDurationMillis = gopDuration; - return this; - } - - public Builder withNalAdaptationFlags(final StreamInfo.NalAdaptationFlags nalAdaptationFlags) { - mNalAdaptationFlags = nalAdaptationFlags; - return this; - } - - public Builder withIsAbsoluteTimecode(final boolean isAbsoluteTimecode) { - mIsAbsoluteTimecode = isAbsoluteTimecode; - return this; - } - - public String getMimeType() { - return mMimeType; - } - - public int getFrameRate() { - return mFrameRate; - } - - public int getmRetentionPeriodInHours() { - return mRetentionPeriodInHours; - } - - public int getHorizontalResolution() { - return mHorizontalResolution; - } - - public int getVerticalResolution() { - return mVerticalResolution; - } - public String getOutputFileName() { - return mOutputFileName; + /** + * Set the camera orientation. + * @param orientation Input value. + * @return Reference to the same builder object and type. + */ + public S withCameraOrientation(final int orientation) { + mCameraOrientation = orientation; + return (S)this; } + /** + * Get the camera ID. + * @return Camera ID string. + */ public String getCameraId() { return mCameraId; } + /** + * Get the camera facing. + * @return Camera facing as an integer value. + */ public int getCameraFacing() { return mCameraFacing; } + /** + * Get the camera orientation. + * @return Camera orientation as an integer value. + */ public int getCameraOrientation() { return mCameraOrientation; } - public int getEncodingBitrate() { - return mEncodingBitrate; - } - - public boolean isEncoderHardwareAccelerated() { - return mIsEncoderHardwareAccelerated; - } - - public int getGopDurationMillis() { - return mGopDurationMillis; - } - - public byte[] getCodecPrivateData() { - return mCodecPrivateData; - } - - public long getFrameTimescale() { - return mFrameTimescale; - } - - public StreamInfo.NalAdaptationFlags getNalAdaptationFlags() { - return mNalAdaptationFlags; - } - + /** + * Method to build the CameraMediaSourceConfiguration using this builder. + * @return Instance of CameraMediaSourceConfiguration. + */ @Override - public CameraMediaSourceConfiguration build() { - return new CameraMediaSourceConfiguration(this); + public T build() { + return (T)new CameraMediaSourceConfiguration(this); } } - private final Builder mBuilder; - - public CameraMediaSourceConfiguration(final Builder builder) { - mBuilder = builder; - } - - @Override - public String getMediaSourceType() { - return MEDIA_SOURCE_TYPE; - } - - @Override - public String getMediaSourceDescription() { - return MEDIA_SOURCE_DESCRIPTION; - } - - public static CameraMediaSourceConfiguration.Builder builder() { - return new CameraMediaSourceConfiguration.Builder(); - } - - /** - * Returns the ID of the camera - */ - public String getCameraId() { - return mBuilder.mCameraId; - } - - /** - * Gets the camera facing front or back. - */ - public int getCameraFacing() { - return mBuilder.mCameraFacing; - } - - /** - * Gets the orientation of the camera in degrees. - */ - public int getCameraOrientation() { - return mBuilder.mCameraOrientation; - } - - /** - * Gets the horizontal resolution. - */ - public int getHorizontalResolution() { - return mBuilder.mHorizontalResolution; - } - - /** - * Gets the vertical resolution. - */ - public int getVerticalResolution() { - return mBuilder.mVerticalResolution; - } - - /** - * Gets the output file name. - */ - public String getOutputFileName() { - return mBuilder.mOutputFileName; - } - - /** - * Gets the frame rate of the camera. - */ - public int getFrameRate() { - return mBuilder.mFrameRate; - } - /** - * Gets the retention period in hours + * Constructor accepting a builder. + * @param builder The builder from which the configuration is to be obtained. */ - public int getRetentionPeriodInHours() { - return mBuilder.mRetentionPeriodInHours; - } - - /** - * Gets the encoding bitrate. - */ - public int getBitRate() { - return mBuilder.mEncodingBitrate; - } - - /** - * Gets the encoder mime type. - */ - @NonNull - public String getEncoderMimeType() { - return mBuilder.mMimeType; + public CameraMediaSourceConfiguration(final Builder builder) { + super(builder); } /** - * Gets the GOP (group-of-pictures) duration in milliseconds. + * Create a new builder for this configuration class. + * @return New instace of CameraMediaSourceConfiguration.Builder. */ - public int getGopDurationMillis() { - return mBuilder.mGopDurationMillis; + public static CameraMediaSourceConfiguration.Builder builder() { + return new CameraMediaSourceConfiguration.Builder<>(); } /** - * Whether the encoder is hardware accelerated. + * Get the camera ID. + * @return Camera ID string. */ - public boolean isEndcoderHardwareAccelerated() { - return mBuilder.mIsEncoderHardwareAccelerated; + public String getCameraId() { + return ((Builder)mBuilder).mCameraId; } /** - * Gets the codec private data. + * Get the camera facing. + * @return Camera facing as an integer value. */ - @Nullable - public byte[] getCodecPrivateData() { - return mBuilder.mCodecPrivateData; + public int getCameraFacing() { + return ((Builder)mBuilder).mCameraFacing; } /** - * Gets the timescale + * Get the camera orientation. + * @return Camera orientation as an integer value. */ - public long getTimeScale() { - return mBuilder.mFrameTimescale; + public int getCameraOrientation() { + return ((Builder)mBuilder).mCameraOrientation; } /** - * Get the Nal Adaption Flag + * Get the media source type string. + * @return Media source type string. */ - public StreamInfo.NalAdaptationFlags getNalAdaptationFlags() { - return mBuilder.mNalAdaptationFlags; + @Override + public String getMediaSourceType() { + return MEDIA_SOURCE_TYPE; } /** - * Get if timecode is absolute or not - * @return + * Get the media source description. + * @return Media source description. */ - public boolean getIsAbsoluteTimecode() { - return mBuilder.mIsAbsoluteTimecode; + @Override + public String getMediaSourceDescription() { + return MEDIA_SOURCE_DESCRIPTION; } } diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderFactory.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderFactory.java index 67b3bef637..8a4b143728 100644 --- a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderFactory.java +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderFactory.java @@ -28,51 +28,61 @@ import android.util.Log; import android.view.Surface; -import com.amazonaws.kinesisvideo.client.mediasource.CameraMediaSourceConfiguration; +import com.amazonaws.kinesisvideo.client.mediasource.AbstractMediaSourceConfiguration; +/** + * Factory class to create and configure emcoders based on a given media configuration. + */ public class EncoderFactory { private static final String TAG = EncoderFactory.class.getSimpleName(); private static final Surface NULL_SURFACE = null; private static final MediaCrypto NULL_CRYPTO = null; private static final int IFRAME_EVERY_2_SEC = 2; + /** + * Creates and configure emcoders based on a given media configuration. + * @param mediaSourceConfiguration The MediaSourceConfiguration to be used to configure the encoder. + * @return The encoder MediaCodec object. + */ public static MediaCodec createConfiguredEncoder( - final CameraMediaSourceConfiguration mediaSourceConfiguration) { + final AbstractMediaSourceConfiguration mediaSourceConfiguration) { return createMediaCodec(mediaSourceConfiguration); } - private static MediaCodec createMediaCodec(final CameraMediaSourceConfiguration mediaSourceConfiguration) { + /** + * Helper fucntion to create a MediaCodec and configure it based on the given MediaSourceConfiguration. + * @param mediaSourceConfiguration The MediaSourceConfiguration to be used to configure the encoder. + * @return The encoder MediaCodec object. + */ + private static MediaCodec createMediaCodec(final AbstractMediaSourceConfiguration mediaSourceConfiguration) { try { final MediaCodec encoder = MediaCodec.createEncoderByType(mediaSourceConfiguration.getEncoderMimeType()); - try { - encoder.configure( - configureMediaFormat(mediaSourceConfiguration, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar), - NULL_SURFACE, - NULL_CRYPTO, - MediaCodec.CONFIGURE_FLAG_ENCODE); - logSupportedColorFormats(encoder, mediaSourceConfiguration); - } catch (MediaCodec.CodecException e) { - Log.d(TAG, "Failed configuring MediaCodec with Semi-planar pixel format, falling back to planar"); - - encoder.configure( - configureMediaFormat(mediaSourceConfiguration, - MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar), - NULL_SURFACE, - NULL_CRYPTO, - MediaCodec.CONFIGURE_FLAG_ENCODE); - logSupportedColorFormats(encoder, mediaSourceConfiguration); - } + // Use YUV420Flexible to be able to support a wide range of devices and scenarios. + encoder.configure( + configureMediaFormat(mediaSourceConfiguration, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible), + NULL_SURFACE, + NULL_CRYPTO, + MediaCodec.CONFIGURE_FLAG_ENCODE); + + logSupportedColorFormats(encoder, mediaSourceConfiguration); return encoder; + } catch (final IOException e) { throw new RuntimeException("unable to create encoder", e); } } + /** + * Helper function to create and prepare a MediaFormat matching the provided MediaSourceConfiguration. + * @param mediaSourceConfiguration The MediaSourceConfiguration to be used to configure the encoder. + * @param colorFormat The MediaFormat object based on the provided configuration. + * @return + */ private static MediaFormat configureMediaFormat( - final CameraMediaSourceConfiguration mediaSourceConfiguration, + final AbstractMediaSourceConfiguration mediaSourceConfiguration, final int colorFormat) { Log.d(TAG, mediaSourceConfiguration.getEncoderMimeType() + " output " @@ -98,9 +108,14 @@ private static MediaFormat configureMediaFormat( return format; } + /** + * Debugging helper function to log all supported color formats of a given encoder. + * @param encoder The MediaCodec encoder to be inspected. + * @param mediaSourceConfiguration The MediaSourceConfiguration used to configure the encoder. + */ private static void logSupportedColorFormats( final MediaCodec encoder, - final CameraMediaSourceConfiguration mediaSourceConfiguration) { + final AbstractMediaSourceConfiguration mediaSourceConfiguration) { final MediaCodecInfo.CodecCapabilities capabilities = encoder.getCodecInfo().getCapabilitiesForType(mediaSourceConfiguration.getEncoderMimeType()); diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderWrapper.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderWrapper.java index 9a628929d4..4f5ca59425 100644 --- a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderWrapper.java +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/encoding/EncoderWrapper.java @@ -21,22 +21,20 @@ import android.media.MediaCodec; import android.util.Log; -import com.amazonaws.kinesisvideo.client.mediasource.CameraMediaSourceConfiguration; +import com.amazonaws.kinesisvideo.client.mediasource.AbstractMediaSourceConfiguration; import com.amazonaws.kinesisvideo.producer.KinesisVideoFrame; import com.amazonaws.mobileconnectors.kinesisvideo.util.FrameUtility; import java.nio.ByteBuffer; /** - * Wrapper class around MediaCodec. - * Accepts raw frame data in YUV420 format as an input, sends it to the encoder, - * notifies the listeners when encoding is complete. - * All happens on the same thread + * Wrapper class around MediaCodec. It accepts raw frame data in YUV420 format as an input, sends it + * to the encoder, and notifies the listeners when encoding is complete. All happens on the same thread. */ public class EncoderWrapper { private static final String TAG = EncoderWrapper.class.getSimpleName(); private static final int TIMEOUT_USEC = 10000; - private final CameraMediaSourceConfiguration mMediaSourceConfiguration; + private final AbstractMediaSourceConfiguration mMediaSourceConfiguration; private MediaCodec mEncoder; private EncoderFrameSubmitter mEncoderFrameSubmitter; private long mLastRecordedFrameTimestamp = 0; @@ -47,19 +45,41 @@ public class EncoderWrapper { private int mFrameIndex; private long mFragmentStart = 0; + /** + * Interface for frame listeners. + */ public interface FrameAvailableListener { - + /** + * Called when a new frame is available. + * @param frame The new frame. + */ void onFrameAvailable(final KinesisVideoFrame frame); } - public interface CodecPrivateDataAvailableListener { + /** + * Interface for codec private data listeners. + */ + public interface CodecPrivateDataAvailableListener { + /** + * Called when new codec private data is available. + * @param privateData + */ void onCodecPrivateDataAvailable(final byte[] privateData); } - public EncoderWrapper(final CameraMediaSourceConfiguration mediaSourceConfiguration) { + + /** + * This constructor creates a new EncoderWrapper using the given MediaSourceConfiguration. + * @param mediaSourceConfiguration MediaSourceConfiguration to use. + */ + public EncoderWrapper(final AbstractMediaSourceConfiguration mediaSourceConfiguration) { mMediaSourceConfiguration = mediaSourceConfiguration; initEncoder(); } + /** + * Helper fucntion to initialize the EncoderWrapper using the MediaSourceConfiguration. + * It creates, configures and starts an encoder. + */ private void initEncoder() { mBufferInfo = new MediaCodec.BufferInfo(); mEncoder = EncoderFactory.createConfiguredEncoder(mMediaSourceConfiguration); @@ -67,36 +87,48 @@ private void initEncoder() { mEncoder.start(); } - public void setCodecPrivateDataAvailableListener( - final CodecPrivateDataAvailableListener listener) { - + /** + * Sets a listener for codec private data. + * @param listener The listener object. + */ + public void setCodecPrivateDataAvailableListener(final CodecPrivateDataAvailableListener listener) { mCodecPrivateDataListener = listener; } + /** + * Sets a listener for new frames. + * @param listener The listener object. + */ public void setEncodedFrameAvailableListener(final FrameAvailableListener listener) { mFrameAvailableListener = listener; } - public void encodeFrame(final Image frameImageYUV420, - final boolean endOfStream) { - + /** + * Encodes a frame using the available encoder, and processes the output of the encoder as well. + * @param frameImageYUV420 The frame to be encoded. + * @param endOfStream True if this is the last frame. + */ + public void encodeFrame(final Image frameImageYUV420, final boolean endOfStream) { + // Edge case. if (mIsStopped) { Log.w(TAG, "received a frame to encode after already stopped. returning"); return; } + // Submit the frame to encoder using EncoderFrameSubmitter. Log.d(TAG, "encoding frame" + threadId()); - mEncoderFrameSubmitter.submitFrameToEncoder(frameImageYUV420, endOfStream); - Log.d(TAG, "frame sent to encoder" + threadId()); + // Process output from encoder. getDataFromEncoder(endOfStream); - Log.d(TAG, "frame encoded" + threadId()); } - + /** + * Deques an output buffer from the encoder and processes it based on the return value from dequeueOutputBuffer. + * @param endOfStream True if the stream has ended. + */ private void getDataFromEncoder(final boolean endOfStream) { boolean stopReadingFromEncoder = false; while(!stopReadingFromEncoder) { @@ -110,73 +142,109 @@ private void getDataFromEncoder(final boolean endOfStream) { } stopReadingFromEncoder = true; break; + case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: break; + default: if (outputBufferId < 0) { Log.w(TAG, "unexpected encoder output buffer id: " + outputBufferId); break; } + // Process output buffer. processEncoderOutputBuffer(outputBufferId); + // Handle end of stream. if (isEndOfStream()) { stopReadingFromEncoder = true; } + // Break after processing one buffer. break; } } } + /** + * Process the given output buffer from the encoder. + * @param outputBufferId Output buffer ID. + */ private void processEncoderOutputBuffer(final int outputBufferId) { + // Edge case. if (mBufferInfo.size == 0) { Log.w(TAG, "empty buffer " + outputBufferId); mEncoder.releaseOutputBuffer(outputBufferId, false); return; } + // Read output buffer contents. final ByteBuffer encodedData = mEncoder.getOutputBuffer(outputBufferId); - if (encodedData == null) { throw new RuntimeException("encoder output buffer " + outputBufferId + " is null"); } + // Process the data. processEncodedData(encodedData); + // Release the buffer. mEncoder.releaseOutputBuffer(outputBufferId, false); } + /** + * Process the data from the output buffer. + * @param encodedData Data from the output buffer. + */ private void processEncodedData(final ByteBuffer encodedData) { + // Position the buffers. adjustEncodedDataPosition(encodedData); adjustEncodedDataPosition(encodedData); + // Handle coded private data. if (isCodecPrivateData()) { notifyCodecPrivateDataAvailable(encodedData); return; } + // Handle end of stream. if (isEndOfStream()) { Log.d(TAG, "end of stream reached"); return; } + // Send the data to KVS producer SDK. sendEncodedFrameToProducerSDK(encodedData); } + /** + * Helper fucntion to position a given buffer correctly. + * @param encodedData The buffer to be used. + */ private void adjustEncodedDataPosition(final ByteBuffer encodedData) { encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); } + /** + * Helper function to check for end of stream flag. + * @return True if the flag is present. + */ private boolean isEndOfStream() { return (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; } + /** + * Helper function to check for codec private data flag. + * @return True if the flag is present. + */ private boolean isCodecPrivateData() { return (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; } + /** + * Helper fucntion to notify codec private data listener. + * @param codecPrivateDataBuffer The data to be passed to the listener. + */ private void notifyCodecPrivateDataAvailable(final ByteBuffer codecPrivateDataBuffer) { Log.d(TAG, "got codec private data"); final ByteBuffer privateData = codecPrivateDataBuffer; @@ -184,6 +252,11 @@ private void notifyCodecPrivateDataAvailable(final ByteBuffer codecPrivateDataBu mCodecPrivateDataListener.onCodecPrivateDataAvailable(codecPrivateDataArray); } + /** + * Helper function to notify frame listener. It uses the data and system time to create a Frame + * to be sent to the listener. + * @param encodedData The frame data to be passed to the listener. + */ private void sendEncodedFrameToProducerSDK(final ByteBuffer encodedData) { final long currentTime = System.currentTimeMillis(); Log.d(TAG, "time between frames: " + (currentTime - mLastRecordedFrameTimestamp) + "ms"); @@ -203,6 +276,9 @@ private void sendEncodedFrameToProducerSDK(final ByteBuffer encodedData) { frameData)); } + /** + * Stops the encoder and releases it. It also uodates the status. + */ public void stop() { Log.d(TAG, "stopping encoder"); mIsStopped = true; @@ -210,16 +286,29 @@ public void stop() { mEncoder.release(); } + /** + * Helper fucntion to convert a ByteBuffer to byte array. + * @param byteBuffer The ByteBuffer to be converted. + * @return The byte[] after conversion. + */ private byte[] convertToArray(final ByteBuffer byteBuffer) { final byte[] array = new byte[byteBuffer.remaining()]; byteBuffer.get(array); return array; } + /** + * Helper fucntion to get a string with Thread ID of the calling thread. + * @return A string with the thread ID. + */ private static String threadId() { return " | threadId=" + Thread.currentThread().getId(); } + /** + * Helper function to sleep for the given millis. + * @param ms Time in millis to sleep. + */ private static void sleep(final int ms) { try { Thread.sleep(ms); diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidCameraMediaSourceConfiguration.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidCameraMediaSourceConfiguration.java index f6576a9686..9145be5040 100644 --- a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidCameraMediaSourceConfiguration.java +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidCameraMediaSourceConfiguration.java @@ -24,40 +24,63 @@ import com.amazonaws.kinesisvideo.producer.StreamInfo; /** - * Parcelable wrapper for CameraMediaSourceConfiguration. Allows passing - * the camera configuration in bundles + * Parcelable wrapper for CameraMediaSourceConfiguration. Allows passing the camera configuration in bundles. */ public class AndroidCameraMediaSourceConfiguration extends CameraMediaSourceConfiguration implements Parcelable { - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - - public AndroidCameraMediaSourceConfiguration createFromParcel(final Parcel in) { - return new AndroidCameraMediaSourceConfiguration(readFromParcel(in)); - } - - public AndroidCameraMediaSourceConfiguration[] newArray(int size) { - return new AndroidCameraMediaSourceConfiguration[size]; + /** + * Generic concrete builder for this configuration class. It's purpose is to provide type arguments + * to the builder defined in CameraMediaSourceConfiguration, and has no new attributes. It uses + * recursive generics pattern to be able to preserve the chaining capability of builders. + * @param MediaSource type. + * @param Builder type. + */ + public static class Builder extends CameraMediaSourceConfiguration.Builder< + AndroidCameraMediaSourceConfiguration, AndroidCameraMediaSourceConfiguration.Builder> { + + /** + * Method to build the AndroidCameraMediaSourceConfiguration using this builder. + * @return Instance of AndroidCameraMediaSourceConfiguration. + */ + @Override + public AndroidCameraMediaSourceConfiguration build() { + return new AndroidCameraMediaSourceConfiguration(this); } - }; + } + /** + * Constructor accepting a builder. + * @param builder The builder from which the configuration is to be obtained. + */ public AndroidCameraMediaSourceConfiguration(final Builder builder) { super(builder); } + /** + * Create a new builder for this configuration class. + * @return New instace of AndroidCameraMediaSourceConfiguration.Builder. + */ public static AndroidCameraMediaSourceConfiguration.Builder builder() { return new Builder(); } + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled + * representation. + * @return Returns 0 since there are no special objects in the marshaled representation. + */ @Override public int describeContents() { return 0; } + /** + * Flatten this object in to a Parcel. + * @param parcel + * @param i + */ @Override - public void writeToParcel(final Parcel parcel, - final int i) { - + public void writeToParcel(final Parcel parcel, final int i) { parcel.writeString(getCameraId()); parcel.writeString(getOutputFileName()); parcel.writeString(getEncoderMimeType()); @@ -73,6 +96,7 @@ public void writeToParcel(final Parcel parcel, parcel.writeInt(getNalAdaptationFlags().getIntValue()); parcel.writeString(String.valueOf((getIsAbsoluteTimecode()))); + // Special handling depending on the presense of codec private data. if (getCodecPrivateData() == null) { parcel.writeInt(0); } else { @@ -81,17 +105,22 @@ public void writeToParcel(final Parcel parcel, } } + /** + * Helper function to hydrate a builder object from a given parcel. + * @param parcel The parcer to be processed. + * @return An instance of AndroidCameraMediaSourceConfiguration.Builder. + */ private static AndroidCameraMediaSourceConfiguration.Builder readFromParcel(final Parcel parcel) { final AndroidCameraMediaSourceConfiguration.Builder builder = new AndroidCameraMediaSourceConfiguration.Builder() + .withCameraFacing(parcel.readInt()) + .withCameraOrientation(parcel.readInt()) .withCameraId(parcel.readString()) .withFileOutput(parcel.readString()) .withEncodingMimeType(parcel.readString()) .withFrameRate(parcel.readInt()) .withHorizontalResolution(parcel.readInt()) .withVerticalResolution(parcel.readInt()) - .withCameraFacing(parcel.readInt()) - .withCameraOrientation(parcel.readInt()) .withEncodingBitRate(parcel.readInt()) .withRetentionPeriodInHours(parcel.readInt()) .withIsEncoderHardwareAccelerated(Boolean.parseBoolean(parcel.readString())) @@ -99,6 +128,7 @@ private static AndroidCameraMediaSourceConfiguration.Builder readFromParcel(fina .withNalAdaptationFlags(StreamInfo.NalAdaptationFlags.getFlag(parcel.readInt())) .withIsAbsoluteTimecode(Boolean.parseBoolean(parcel.readString())); + // Special handling depending on the presense of codec private data. final int codecPrivateDataSize = parcel.readInt(); if (codecPrivateDataSize > 0) { final byte[] privateData = new byte[codecPrivateDataSize]; @@ -108,4 +138,29 @@ private static AndroidCameraMediaSourceConfiguration.Builder readFromParcel(fina return builder; } + + /** + * This field holding an object implementing Parcelable.Creator is neeeded according to Android Parceable contract. + */ + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + /** + * Create a new instance of the Parcelable class, instantiating it from the given Parcel. + * @param in The parcel to be deserialized. + * @return An instance of AndroidCameraMediaSourceConfiguration. + */ + public AndroidCameraMediaSourceConfiguration createFromParcel(final Parcel in) { + return new AndroidCameraMediaSourceConfiguration(readFromParcel(in)); + } + + /** + * Create a new array of the Parcelable class. + * @param size Length of the array. + * @return An array of AndroidCameraMediaSourceConfiguration of the given length. + */ + public AndroidCameraMediaSourceConfiguration[] newArray(int size) { + return new AndroidCameraMediaSourceConfiguration[size]; + } + }; } diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidMediaSourceFactory.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidMediaSourceFactory.java index aecbc5ae5e..f9bc6a8735 100644 --- a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidMediaSourceFactory.java +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidMediaSourceFactory.java @@ -24,21 +24,43 @@ import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceConfiguration; import com.amazonaws.kinesisvideo.client.mediasource.UnknownMediaSourceException; +/** + * Factory class for media sources. + */ public final class AndroidMediaSourceFactory { + /** + * Creates a new media source object based on the confirguration provided. Currently supports + * AndroidCameraMediaSource and AndroidUrlMediaSource only. + * @param streamName Name of the KVS stream. + * @param context Android context object. + * @param configuration Media source configuration to be used. + * @return A new MediaSource object. + */ public static MediaSource createMediaSource( final String streamName, final Context context, final MediaSourceConfiguration configuration) { - if (CameraMediaSourceConfiguration.MEDIA_SOURCE_TYPE - .equals(configuration.getMediaSourceType())) { + // Check the type of media source and invoke the corresponding helper methods. + if (CameraMediaSourceConfiguration.MEDIA_SOURCE_TYPE.equals(configuration.getMediaSourceType())) { return createAndroidCameraMediaSource(streamName, context, (CameraMediaSourceConfiguration) configuration); + + } if (AndroidUrlMediaSourceConfiguration.MEDIA_SOURCE_TYPE.equals(configuration.getMediaSourceType())) { + return createAndroidUrlMediaSource(streamName, (AndroidUrlMediaSourceConfiguration) configuration); + } else { throw new UnknownMediaSourceException(configuration.getMediaSourceType()); } } + /** + * Helper method to create an instance of AndroidCameraMediaSource based on the confirguration provided. + * @param streamName Name of the KVS stream. + * @param context Android context object. + * @param configuration Media source configuration to be used. + * @return An instance of AndroidCameraMediaSource. + */ private static AndroidCameraMediaSource createAndroidCameraMediaSource( final String streamName, final Context context, @@ -49,6 +71,24 @@ private static AndroidCameraMediaSource createAndroidCameraMediaSource( return mediaSource; } + /** + * Helper method to create an instance of AndroidUrlMediaSource based on the confirguration provided. + * @param streamName Name of the KVS stream. + * @param configuration Media source configuration to be used. + * @return An instance of AndroidUrlMediaSource. + */ + private static AndroidUrlMediaSource createAndroidUrlMediaSource( + final String streamName, + final AndroidUrlMediaSourceConfiguration configuration) { + + final AndroidUrlMediaSource mediaSource = new AndroidUrlMediaSource(streamName); + mediaSource.configure(configuration); + return mediaSource; + } + + /** + * Private constructor to prevent instantiation of this class. + */ private AndroidMediaSourceFactory() { // no-op } diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSource.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSource.java new file mode 100644 index 0000000000..8536df1902 --- /dev/null +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSource.java @@ -0,0 +1,481 @@ +/** + * Copyright 2017-2018 Amazon.com, + * Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Amazon Software License (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/asl/ + * + * or in the "license" file accompanying this file. This file is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, express or implied. See the License + * for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.mobileconnectors.kinesisvideo.mediasource.android; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.media.Image; +import android.media.ImageReader; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.util.Log; +import android.view.Surface; + +import com.amazonaws.kinesisvideo.producer.StreamCallbacks; +import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSource; +import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceConfiguration; +import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceSink; +import com.amazonaws.mobileconnectors.kinesisvideo.mediasource.android.AndroidUrlMediaSourceConfiguration; +import com.amazonaws.kinesisvideo.client.mediasource.MediaSourceState; +import com.amazonaws.kinesisvideo.common.exception.KinesisVideoException; +import com.amazonaws.kinesisvideo.producer.KinesisVideoFrame; +import com.amazonaws.kinesisvideo.producer.StreamInfo; +import com.amazonaws.kinesisvideo.producer.Tag; +import com.amazonaws.mobileconnectors.kinesisvideo.camera.CameraFramesSource; +import com.amazonaws.mobileconnectors.kinesisvideo.encoding.EncoderWrapper; + +import static com.amazonaws.kinesisvideo.producer.Time.HUNDREDS_OF_NANOS_IN_AN_HOUR; +import static com.amazonaws.kinesisvideo.producer.Time.NANOS_IN_A_TIME_UNIT; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.DEFAULT_BUFFER_DURATION; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.DEFAULT_GOP_DURATION; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.DEFAULT_REPLAY_DURATION; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.DEFAULT_STALENESS_DURATION; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.KEYFRAME_FRAGMENTATION; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.NOT_ADAPTIVE; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.NO_KMS_KEY_ID; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.RECALCULATE_METRICS; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.RECOVER_ON_FAILURE; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.REQUEST_FRAGMENT_ACKS; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.SDK_GENERATES_TIMECODES; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.VERSION_ZERO; +import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.MAX_LATENCY; + +/** + * Android Url MediaSource. + */ +public class AndroidUrlMediaSource implements MediaSource { + private static final String TAG = AndroidUrlMediaSource.class.getSimpleName(); + + private final String mStreamName; + private MediaSourceState mMediaSourceState; + private AndroidUrlMediaSourceConfiguration mMediaSourceConfiguration; + private MediaSourceSink mMediaSourceSink; + private EncoderWrapper mEncoderWrapper; + private Thread mDecoderThread; + + /** + * Constructor to create a new AndroidUrlMediaSource given a KVS stream name. + * @param streamName KVS Stream name. + */ + public AndroidUrlMediaSource(final String streamName) { + mStreamName = streamName; + } + + /** + * Get the current state of this media source. + * @return Current MediaSourceState. + */ + @Override + public MediaSourceState getMediaSourceState() { + return mMediaSourceState; + } + + /** + * Get current configuration of this media source. + * @return Current MediaSourceConfiguration. + */ + @Override + public MediaSourceConfiguration getConfiguration() { + return mMediaSourceConfiguration; + } + + /** + * Get the StreamInfo for this media source based on the current configuration. + * @return StreamInfo object. + * @throws KinesisVideoException + */ + @Override + public StreamInfo getStreamInfo() throws KinesisVideoException { + // Need to fix-up the content type as the Console playback only accepts video/h264 and will fail + // if the mime type is video/avc which is the default in Android. + String contentType = mMediaSourceConfiguration.getEncoderMimeType(); + if (contentType.equals("video/avc")) { + contentType = "video/h264"; + } + + return new StreamInfo(VERSION_ZERO, + mStreamName, + StreamInfo.StreamingType.STREAMING_TYPE_REALTIME, + contentType, + NO_KMS_KEY_ID, + mMediaSourceConfiguration.getRetentionPeriodInHours() + * HUNDREDS_OF_NANOS_IN_AN_HOUR, + NOT_ADAPTIVE, + MAX_LATENCY, + DEFAULT_GOP_DURATION, + KEYFRAME_FRAGMENTATION, + SDK_GENERATES_TIMECODES, + mMediaSourceConfiguration.getIsAbsoluteTimecode(), + REQUEST_FRAGMENT_ACKS, + RECOVER_ON_FAILURE, + StreamInfo.codecIdFromContentType(mMediaSourceConfiguration.getEncoderMimeType()), + StreamInfo.createTrackName(mMediaSourceConfiguration.getEncoderMimeType()), + mMediaSourceConfiguration.getBitRate(), + mMediaSourceConfiguration.getFrameRate(), + DEFAULT_BUFFER_DURATION, + DEFAULT_REPLAY_DURATION, + DEFAULT_STALENESS_DURATION, + mMediaSourceConfiguration.getTimeScale() / NANOS_IN_A_TIME_UNIT, + RECALCULATE_METRICS, + mMediaSourceConfiguration.getCodecPrivateData(), + new Tag[] { + new Tag("device", "Test Device"), + new Tag("stream", "Test Stream") }, + mMediaSourceConfiguration.getNalAdaptationFlags()); + } + + /** + * Initialize this media source with given MediaSourceSink. This method will be called by the SDK. + * @param mediaSourceSink The MediaSourceSink object to use. + * @throws KinesisVideoException + */ + @Override + public void initialize(@NonNull final MediaSourceSink mediaSourceSink) throws KinesisVideoException { + mMediaSourceSink = mediaSourceSink; + mMediaSourceState = MediaSourceState.INITIALIZED; + } + + /** + * Configures this media source with the given MediaSourceConfiguration. + * @param configuration + */ + @Override + public void configure(final MediaSourceConfiguration configuration) { + if (!(configuration instanceof AndroidUrlMediaSourceConfiguration)) { + throw new IllegalArgumentException( + "expected instance of AndroidUrlMediaSourceConfiguration" + + ", received " + configuration); + } + + mMediaSourceConfiguration = (AndroidUrlMediaSourceConfiguration) configuration; + createFramesSource(); + } + + /** + * Start the decoding of the video url. It starts a new thread and processses the video decoding + * on that thread. + * @throws KinesisVideoException + */ + @Override + public void start() throws KinesisVideoException { + mMediaSourceState = MediaSourceState.RUNNING; + startDecoding(); + } + + /** + * Stop the decoding of the video url. This is a no-op if this media source is already stopped. + * @throws KinesisVideoException + */ + @Override + public void stop() throws KinesisVideoException { + mMediaSourceState = MediaSourceState.STOPPED; + stopDecoding(); + } + + /** + * Checks if this media source is stopped. + * @return True if stopped. + */ + @Override + public boolean isStopped() { + return mMediaSourceState == MediaSourceState.STOPPED; + } + + /** + * Free resources for this media source. It just calls stop which will release the resources. + */ + @Override + public void free() throws KinesisVideoException { + stop(); + } + + /** + * This is a no-op for this media source class. + * @return + */ + @Nullable + @Override + public StreamCallbacks getStreamCallbacks() { + return null; + } + + /** + * Get the MediaSourceSink object associatea with this media source. + * @return Associated MediaSourceSink object. + */ + @Override + public MediaSourceSink getMediaSourceSink() { + return mMediaSourceSink; + } + + /** + * Helper method to create and configure EncoderWrapper as well as setup callbacks. + */ + private synchronized void createFramesSource() { + try { + mEncoderWrapper = new EncoderWrapper(mMediaSourceConfiguration); + mEncoderWrapper.setCodecPrivateDataAvailableListener(waitForCodecPrivateData()); + mEncoderWrapper.setEncodedFrameAvailableListener(pushFrameToSink()); + } catch (final Throwable e) { + Log.e(TAG, "EncoderWrapper init exception" + threadId(), e); + } + } + + /** + * Thread safe method to start this media source. It creates a new thread and starts decoding the + * url on this thread. The thread is kept alive until stopDecoding call or EOS occurs. + */ + private synchronized void startDecoding() { + Log.i(TAG, "Decoding starting"); + + mDecoderThread = new Thread(() -> decodeVideoToYUV(mMediaSourceConfiguration.getUrl())); + mDecoderThread.start(); + + Log.i(TAG, "Decoding started"); + } + + /** + * Thread safe method to stop this media source. It blocks till the decoder thread has exited. + */ + private synchronized void stopDecoding() { + Log.i(TAG, "Decoding stopping"); + + mEncoderWrapper = null; + + while(mDecoderThread != null){ + try{ + this.wait(50); + }catch (InterruptedException e){ + e.printStackTrace(); + } + } + + Log.i(TAG, "Decoding stopped"); + } + + /** + * This is the main entry point of the threads. It created and configures MediaExtractor and decoder + * to create YUV frames from the video url. It blocks until a stopDecoding call or EOS. + * @param videoUrl + */ + private void decodeVideoToYUV(String videoUrl) { + MediaExtractor extractor = null; + MediaCodec decoder = null; + + try { + // Create and configure the MediaExtractor. + extractor = new MediaExtractor(); + extractor.setDataSource(videoUrl); + + // Identify video track index. + int trackIndex = selectTrack(extractor); + if (trackIndex < 0) { + Log.e(TAG, "No video track found in " + videoUrl); + return; + } + + // Create decoder for the video track. + extractor.selectTrack(trackIndex); + MediaFormat format = extractor.getTrackFormat(trackIndex); + decoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)); + + // Configure and start the decoder. + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); + decoder.configure(format, null, null, 0); + decoder.start(); + + // Process the entire video. + decodeVideo(extractor, decoder); + + } catch (IOException e) { + Log.e(TAG, "Error setting data source", e); + + } finally { + Log.i(TAG, "Decoding thread clean up begin."); + // Clean up in a thread safe way. + synchronized (AndroidUrlMediaSource.this){ + if (decoder != null) { + decoder.stop(); + decoder.release(); + } + extractor.release(); + + mMediaSourceState = MediaSourceState.STOPPED; + + mEncoderWrapper = null; + mDecoderThread = null; + } + Log.i(TAG, "Decoding thread clean up done."); + } + } + + /** + * Helper function to identify video track index. + * @param extractor Media extractor object configured with the video source. + * @return Video track index. + */ + private int selectTrack(MediaExtractor extractor) { + int numTracks = extractor.getTrackCount(); + for (int i = 0; i < numTracks; i++) { + MediaFormat format = extractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + + // Return index of the first video track found. + if (mime.startsWith("video/")) { + return i; + } + } + return -1; + } + + /** + * Blocking function to process a complete video source all the way to EOS or until it was interrupted. + * @param extractor MediaExtractor with video sources configured on it. + * @param codec MediaCodec decoder with configuration already applied on it. + */ + private void decodeVideo(MediaExtractor extractor, MediaCodec codec) { + final int TIMEOUT_US = 10000; + MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + + // Loop over the entire length of the video source. + boolean isEOS = false; + outer: + while (!Thread.interrupted()) { + // Read from MediaExtractor and feed inut buffers of the decoder until EOS was found. + if (!isEOS) { + int inIndex = codec.dequeueInputBuffer(TIMEOUT_US); + if (inIndex >= 0) { + ByteBuffer buffer = codec.getInputBuffer(inIndex); + int sampleSize = extractor.readSampleData(buffer, 0); + if (sampleSize < 0) { + // End of stream + codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + isEOS = true; + } else { + codec.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0); + extractor.advance(); + } + } + } + + // Get decoder output buffer and retrieve YUV Image and pass the same on to EncoderWrapper. + int outIndex = codec.dequeueOutputBuffer(info, TIMEOUT_US); + switch (outIndex) { + case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: + break; + + case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: + MediaFormat format = codec.getOutputFormat(); + Log.d(TAG, "New format " + format); + break; + + case MediaCodec.INFO_TRY_AGAIN_LATER: + break; + + default: + if (info.size == 0) { + Log.w(TAG, "empty buffer " + outIndex); + codec.releaseOutputBuffer(outIndex, false); + break; + } + + // Get outout Image object. + final Image yuvImage = codec.getOutputImage(outIndex); + if (yuvImage == null) { + throw new RuntimeException("decoder output image " + outIndex + " is null"); + } + + // Pass to EncoderWrapper in thread safe way. + synchronized (AndroidUrlMediaSource.this) { + if (mEncoderWrapper == null) { + Log.i(TAG, "Quitting decoding loop."); + break outer; + } + + mEncoderWrapper.encodeFrame(yuvImage, isEOS); + } + + codec.releaseOutputBuffer(outIndex, false); + break; + } + + // Break off if all decoded frames have been rendered. + if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + } + } + + /** + * Helper function to return a listener object for codec private data. The listener will eventually + * pass the data on to the KVS producer SDK's MediaSourceSink object. + * @return Listener object. + */ + private EncoderWrapper.CodecPrivateDataAvailableListener waitForCodecPrivateData() { + return new EncoderWrapper.CodecPrivateDataAvailableListener() { + @Override + public void onCodecPrivateDataAvailable(final byte[] privateData) { + try { + Log.i(TAG, "updating sink with codec private data"); + mMediaSourceSink.onCodecPrivateData(privateData); + } catch (final KinesisVideoException e) { + Log.e(TAG, "error updating sink with codec private data", e); + throw new RuntimeException("error updating sink with codec private data", e); + } + } + }; + } + + /** + * Helper function to return a listener object for frame data. The listener will eventually + * pass the data on to the KVS producer SDK's MediaSourceSink object. + * @return Listener object. + */ + private EncoderWrapper.FrameAvailableListener pushFrameToSink() { + return new EncoderWrapper.FrameAvailableListener() { + @Override + public void onFrameAvailable(final KinesisVideoFrame frame) { + try { + Log.i(TAG, "updating sink with frame"); + mMediaSourceSink.onFrame(frame); + } catch (final KinesisVideoException e) { + Log.e(TAG, "error updating sink with frame", e); + } + } + }; + } + + /** + * Helper function to return a string with the thread ID of the current thread. + * @return String with thread ID of the current thread. + */ + private static String threadId() { + return " | threadId=" + Thread.currentThread().getId(); + } +} diff --git a/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfiguration.java b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfiguration.java new file mode 100644 index 0000000000..7a7423afaf --- /dev/null +++ b/aws-android-sdk-kinesisvideo/src/main/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfiguration.java @@ -0,0 +1,123 @@ +/** + * Copyright 2017-2018 Amazon.com, + * Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Amazon Software License (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/asl/ + * + * or in the "license" file accompanying this file. This file is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, express or implied. See the License + * for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazonaws.mobileconnectors.kinesisvideo.mediasource.android; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.amazonaws.kinesisvideo.producer.StreamInfo; +import com.amazonaws.kinesisvideo.client.mediasource.AbstractMediaSourceConfiguration; + +/** + * This class represents a MediaSourceConfiguration for a MediaSource based on a video URL. + * It can work with any valid URL both local and remote. + */ +public class AndroidUrlMediaSourceConfiguration extends AbstractMediaSourceConfiguration { + + /** + * Description string for this media source configuration class. + */ + private static final String MEDIA_SOURCE_DESCRIPTION = "Configuration for a url media source"; + + /** + * Unique type string for this media source configuration class. + */ + public static final String MEDIA_SOURCE_TYPE = "UrlMediaSource"; + + /** + * Generic concrete builder for this configuration class. It's purpose is to provide type arguments + * to the builder defined in AndroidUrlMediaSourceConfiguration. It uses recursive generics pattern + * to be able to preserve the chaining capability of builders. + * @param MediaSource type. + * @param Builder type. + */ + public static class Builder extends AbstractMediaSourceConfiguration.Builder< + AndroidUrlMediaSourceConfiguration, AndroidUrlMediaSourceConfiguration.Builder> { + + private String mUrl; + + /** + * Set the video url. + * @param url Input value. + * @return Reference to the same builder object and type. + */ + public Builder withUrl(final String url) { + mUrl = url; + return this; + } + + /** + * Get the video url. + * @return Video url string. + */ + public String getUrl() { + return mUrl; + } + + /** + * Method to build the AndroidUrlMediaSourceConfiguration using this builder. + * @return Instance of AndroidUrlMediaSourceConfiguration. + */ + @Override + public AndroidUrlMediaSourceConfiguration build() { + return new AndroidUrlMediaSourceConfiguration(this); + } + } + + /** + * Constructor accepting a builder. + * @param builder The builder from which the configuration is to be obtained. + */ + public AndroidUrlMediaSourceConfiguration(final Builder builder) { + super(builder); + } + + /** + * Create a new builder for this configuration class. + * @return New instace of AndroidUrlMediaSourceConfiguration.Builder. + */ + public static AndroidUrlMediaSourceConfiguration.Builder builder() { + return new AndroidUrlMediaSourceConfiguration.Builder(); + } + + /** + * Get the media source type string. + * @return Media source type string. + */ + @Override + public String getMediaSourceType() { + return MEDIA_SOURCE_TYPE; + } + + /** + * Get the media source description. + * @return Media source description. + */ + @Override + public String getMediaSourceDescription() { + return MEDIA_SOURCE_DESCRIPTION; + } + + /** + * Get the video url. + * @return Video url string. + */ + public String getUrl() { + return ((Builder)mBuilder).mUrl; + } +} diff --git a/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfigurationTest.java b/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfigurationTest.java new file mode 100644 index 0000000000..f14442ed7b --- /dev/null +++ b/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceConfigurationTest.java @@ -0,0 +1,51 @@ +package com.amazonaws.mobileconnectors.kinesisvideo.mediasource.android; + +import com.amazonaws.kinesisvideo.producer.StreamInfo; +import com.amazonaws.mobileconnectors.kinesisvideo.data.MimeType; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class AndroidUrlMediaSourceConfigurationTest { + + final String URL = "test-url"; + final String ENCODING_MIME_TYPE = MimeType.H264.getMimeType(); + final int HORIZONTAL_RESOLUTION = 480; + final int VERTICAL_RESOLUTION = 360; + final boolean IS_HARDWARE_ACCELERATED = false; + final byte[] CODEC_PRIVATE_DATA = new byte[10]; + final int FRAMERATE = 60; + final int RETENTION_PERIOD_IN_HOURS = 48; + final int ENCODING_BIT_RATE = 384; + final StreamInfo.NalAdaptationFlags NAL_ADAPTION_FLAGS = StreamInfo.NalAdaptationFlags.NAL_ADAPTATION_ANNEXB_CPD_NALS; + final boolean IS_ABSOLUTE_TIMECODE = false; + + @Test + public void testConfigurationFieldsAreSetCorrectly() { + AndroidUrlMediaSourceConfiguration configuration = new AndroidUrlMediaSourceConfiguration( + AndroidUrlMediaSourceConfiguration.builder() + .withUrl(URL) + .withEncodingMimeType(ENCODING_MIME_TYPE) + .withHorizontalResolution(HORIZONTAL_RESOLUTION) + .withVerticalResolution(VERTICAL_RESOLUTION) + .withIsEncoderHardwareAccelerated(IS_HARDWARE_ACCELERATED) + .withFrameRate(FRAMERATE) + .withRetentionPeriodInHours(RETENTION_PERIOD_IN_HOURS) + .withEncodingBitRate(ENCODING_BIT_RATE) + .withCodecPrivateData(CODEC_PRIVATE_DATA) + .withNalAdaptationFlags(NAL_ADAPTION_FLAGS) + .withIsAbsoluteTimecode(IS_ABSOLUTE_TIMECODE)); + + assertThat(configuration.getUrl()).isEqualTo(URL); + assertThat(configuration.getEncoderMimeType()).isEqualTo(ENCODING_MIME_TYPE); + assertThat(configuration.getHorizontalResolution()).isEqualTo(HORIZONTAL_RESOLUTION); + assertThat(configuration.getVerticalResolution()).isEqualTo(VERTICAL_RESOLUTION); + assertThat(configuration.isEndcoderHardwareAccelerated()).isEqualTo(IS_HARDWARE_ACCELERATED); + assertThat(configuration.getCodecPrivateData()).isEqualTo(CODEC_PRIVATE_DATA); + assertThat(configuration.getFrameRate()).isEqualTo(FRAMERATE); + assertThat(configuration.getRetentionPeriodInHours()).isEqualTo(RETENTION_PERIOD_IN_HOURS); + assertThat(configuration.getBitRate()).isEqualTo(ENCODING_BIT_RATE); + assertThat(configuration.getNalAdaptationFlags()).isEqualTo(NAL_ADAPTION_FLAGS); + assertThat(configuration.getIsAbsoluteTimecode()).isEqualTo(IS_ABSOLUTE_TIMECODE); + } +} diff --git a/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceTest.java b/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceTest.java new file mode 100644 index 0000000000..1113f60ece --- /dev/null +++ b/aws-android-sdk-kinesisvideo/src/test/java/com/amazonaws/mobileconnectors/kinesisvideo/mediasource/android/AndroidUrlMediaSourceTest.java @@ -0,0 +1,125 @@ +package com.amazonaws.mobileconnectors.kinesisvideo.mediasource.android; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import com.amazonaws.kinesisvideo.client.mediasource.MediaSourceState; +import com.amazonaws.kinesisvideo.common.exception.KinesisVideoException; +import com.amazonaws.kinesisvideo.internal.client.mediasource.MediaSourceSink; +import com.amazonaws.kinesisvideo.internal.producer.KinesisVideoProducerStream; +import com.amazonaws.kinesisvideo.producer.KinesisVideoFrame; +import com.amazonaws.kinesisvideo.producer.StreamInfo; +import com.amazonaws.mobileconnectors.kinesisvideo.data.MimeType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = {Config.OLDEST_SDK}) +public class AndroidUrlMediaSourceTest { + + private final String STREAM_NAME = "Test_Name"; + private final String URL = "test-url"; + private final String ENCODING_MIME_TYPE = MimeType.H264.getMimeType(); + private final int HORIZONTAL_RESOLUTION = 480; + private final int VERTICAL_RESOLUTION = 360; + private final boolean IS_HARDWARE_ACCELERATED = false; + private final byte[] CODEC_PRIVATE_DATA = new byte[10]; + private final int FRAMERATE = 60; + private final int RETENTION_PERIOD_IN_HOURS = 48; + private final int ENCODING_BIT_RATE = 384; + private final StreamInfo.NalAdaptationFlags NAL_ADAPTION_FLAGS = StreamInfo.NalAdaptationFlags.NAL_ADAPTATION_ANNEXB_CPD_NALS; + private final boolean IS_ABSOLUTE_TIMECODE = false; + private Context context; + private AndroidUrlMediaSource urlMediaSource; + private AndroidUrlMediaSourceConfiguration configuration; + + @Before + public void initialize() { + context = ApplicationProvider.getApplicationContext(); + configuration = new AndroidUrlMediaSourceConfiguration( + AndroidUrlMediaSourceConfiguration.builder() + .withUrl(URL) + .withEncodingMimeType(ENCODING_MIME_TYPE) + .withHorizontalResolution(HORIZONTAL_RESOLUTION) + .withVerticalResolution(VERTICAL_RESOLUTION) + .withIsEncoderHardwareAccelerated(IS_HARDWARE_ACCELERATED) + .withFrameRate(FRAMERATE) + .withRetentionPeriodInHours(RETENTION_PERIOD_IN_HOURS) + .withEncodingBitRate(ENCODING_BIT_RATE) + .withCodecPrivateData(CODEC_PRIVATE_DATA) + .withNalAdaptationFlags(NAL_ADAPTION_FLAGS) + .withIsAbsoluteTimecode(IS_ABSOLUTE_TIMECODE)); + } + + @Test + public void testFreeMediaSource() throws KinesisVideoException { + urlMediaSource = new AndroidUrlMediaSource(STREAM_NAME); + urlMediaSource.free(); + } + + @Test + public void testConfigurationFieldsAreSetCorrectly() throws KinesisVideoException { + urlMediaSource = new AndroidUrlMediaSource(STREAM_NAME); + urlMediaSource.configure(configuration); + + assertThat(urlMediaSource.getStreamInfo().getName()).isEqualTo(STREAM_NAME); + + AndroidUrlMediaSourceConfiguration mediaSourceConfiguration = (AndroidUrlMediaSourceConfiguration) urlMediaSource.getConfiguration(); + assertThat(mediaSourceConfiguration.getUrl()).isEqualTo(URL); + assertThat(mediaSourceConfiguration.getEncoderMimeType()).isEqualTo(ENCODING_MIME_TYPE); + assertThat(mediaSourceConfiguration.getHorizontalResolution()).isEqualTo(HORIZONTAL_RESOLUTION); + assertThat(mediaSourceConfiguration.getVerticalResolution()).isEqualTo(VERTICAL_RESOLUTION); + assertThat(mediaSourceConfiguration.isEndcoderHardwareAccelerated()).isEqualTo(IS_HARDWARE_ACCELERATED); + assertThat(mediaSourceConfiguration.getCodecPrivateData()).isEqualTo(CODEC_PRIVATE_DATA); + assertThat(mediaSourceConfiguration.getFrameRate()).isEqualTo(FRAMERATE); + assertThat(mediaSourceConfiguration.getRetentionPeriodInHours()).isEqualTo(RETENTION_PERIOD_IN_HOURS); + assertThat(mediaSourceConfiguration.getBitRate()).isEqualTo(ENCODING_BIT_RATE); + assertThat(mediaSourceConfiguration.getNalAdaptationFlags()).isEqualTo(NAL_ADAPTION_FLAGS); + assertThat(mediaSourceConfiguration.getIsAbsoluteTimecode()).isEqualTo(IS_ABSOLUTE_TIMECODE); + + urlMediaSource.free(); + } + + @Test + public void testInitializedState() throws KinesisVideoException { + urlMediaSource = new AndroidUrlMediaSource(STREAM_NAME); + urlMediaSource.configure(configuration); + MediaSourceSink sink = new MediaSourceSink() { + @Override + public void onFrame(@NonNull KinesisVideoFrame kinesisVideoFrame) throws KinesisVideoException { + + } + + @Override + public void onCodecPrivateData(@Nullable byte[] codecPrivateData) throws KinesisVideoException { + + } + + @Override + public void onCodecPrivateData(@Nullable byte[] codecPrivateData, int trackId) throws KinesisVideoException { + + } + + @Override + public void onFragmentMetadata(@NonNull String metadataName, @NonNull String metadataValue, boolean persistent) throws KinesisVideoException { + + } + + @Override + public KinesisVideoProducerStream getProducerStream() { + return null; + } + }; + urlMediaSource.initialize(sink); + + assertThat(urlMediaSource.getMediaSourceState()).isEqualTo(MediaSourceState.INITIALIZED); + assertThat(urlMediaSource.getMediaSourceSink()).isEqualTo(sink); + urlMediaSource.free(); + } +}