/** * 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.internal.mediasource.bytes; import static com.amazonaws.kinesisvideo.producer.FrameFlags.FRAME_FLAG_KEY_FRAME; import static com.amazonaws.kinesisvideo.producer.FrameFlags.FRAME_FLAG_NONE; import static com.amazonaws.kinesisvideo.producer.StreamInfo.NalAdaptationFlags.NAL_ADAPTATION_FLAG_NONE; import static com.amazonaws.kinesisvideo.producer.Time.HUNDREDS_OF_NANOS_IN_AN_HOUR; import static com.amazonaws.kinesisvideo.producer.Time.HUNDREDS_OF_NANOS_IN_A_MILLISECOND; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.ABSOLUTE_TIMECODES; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.DEFAULT_BITRATE; 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.DEFAULT_TIMESCALE; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.FRAMERATE_30; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.KEYFRAME_FRAGMENTATION; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.MAX_LATENCY_ZERO; 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.USE_FRAME_TIMECODES; import static com.amazonaws.kinesisvideo.util.StreamInfoConstants.VERSION_ZERO; import java.nio.ByteBuffer; import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.kinesisvideo.client.mediasource.MediaSourceState; import com.amazonaws.kinesisvideo.common.exception.KinesisVideoException; import com.amazonaws.kinesisvideo.common.preconditions.Preconditions; import com.amazonaws.kinesisvideo.internal.mediasource.OnStreamDataAvailable; import com.amazonaws.kinesisvideo.producer.KinesisVideoFrame; import com.amazonaws.kinesisvideo.producer.StreamCallbacks; import com.amazonaws.kinesisvideo.producer.StreamInfo; import com.amazonaws.kinesisvideo.producer.Tag; public class BytesMediaSource implements MediaSource { private static final String TAG = "BytesMediaSource"; private static final int KEY_FRAME_EVERY_60_FRAMES = 60; private static final long DEFAULT_FRAME_DURATION_33MS = 33L; private final String streamName; private BytesMediaSourceConfiguration configuration; private MediaSourceState mediaSourceState; private MediaSourceSink mediaSourceSink; private BytesGenerator bytesGenerator; private int frameIndex; private long lastTimestampMillis; public BytesMediaSource(final @NonNull String streamName) { this.streamName = streamName; } @Override public MediaSourceState getMediaSourceState() { return mediaSourceState; } @Override public MediaSourceConfiguration getConfiguration() { return configuration; } @Override public StreamInfo getStreamInfo() { return new StreamInfo(VERSION_ZERO, streamName, StreamInfo.StreamingType.STREAMING_TYPE_REALTIME, "application/octet-stream", NO_KMS_KEY_ID, configuration.getRetentionPeriodInHours() * HUNDREDS_OF_NANOS_IN_AN_HOUR, NOT_ADAPTIVE, MAX_LATENCY_ZERO, DEFAULT_GOP_DURATION * HUNDREDS_OF_NANOS_IN_A_MILLISECOND, KEYFRAME_FRAGMENTATION, USE_FRAME_TIMECODES, ABSOLUTE_TIMECODES, REQUEST_FRAGMENT_ACKS, RECOVER_ON_FAILURE, null, null, DEFAULT_BITRATE, FRAMERATE_30, DEFAULT_BUFFER_DURATION, DEFAULT_REPLAY_DURATION, DEFAULT_STALENESS_DURATION, DEFAULT_TIMESCALE, RECALCULATE_METRICS, null, new Tag[] { new Tag("device", "Test Device"), new Tag("stream", "Test Stream") }, NAL_ADAPTATION_FLAG_NONE); } @Override public void initialize(@NonNull final MediaSourceSink mediaSourceSink) throws KinesisVideoException { this.mediaSourceSink = mediaSourceSink; } @Override public void configure(final MediaSourceConfiguration configuration) { Preconditions.checkState(this.configuration == null); if (!(configuration instanceof BytesMediaSourceConfiguration)) { throw new IllegalArgumentException("can only use BytesMediaSourceConfiguration"); } this.configuration = (BytesMediaSourceConfiguration) configuration; } @Override public void start() throws KinesisVideoException { mediaSourceState = MediaSourceState.RUNNING; bytesGenerator = new BytesGenerator(configuration.getFps()); bytesGenerator.onStreamDataAvailable(createDataAvailableCallback()); bytesGenerator.start(); } private OnStreamDataAvailable createDataAvailableCallback() { return new OnStreamDataAvailable() { @Override public void onFrameDataAvailable(final ByteBuffer data) { final long currentTimeMs = System.currentTimeMillis(); final long decodingTs = currentTimeMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND; final long presentationTs = currentTimeMs * HUNDREDS_OF_NANOS_IN_A_MILLISECOND; final long msSinceLastFrame = currentTimeMs - lastTimestampMillis; final long frameDuration = lastTimestampMillis == 0 ? DEFAULT_FRAME_DURATION_33MS * HUNDREDS_OF_NANOS_IN_A_MILLISECOND : msSinceLastFrame * HUNDREDS_OF_NANOS_IN_A_MILLISECOND / 2; final int flags = isKeyFrame() ? FRAME_FLAG_KEY_FRAME : FRAME_FLAG_NONE; final KinesisVideoFrame frame = new KinesisVideoFrame( frameIndex++, flags, decodingTs, presentationTs, frameDuration, data); // ignore frame of size 0 or duration of 0 if (frame.getSize() == 0 || frameDuration == 0) { return; } lastTimestampMillis = currentTimeMs; submitFrameOnUIThread(frame); } @Override public void onFragmentMetadataAvailable(final String metadataName, final String metadataValue, final boolean persistent) { try { mediaSourceSink.onFragmentMetadata(metadataName, metadataValue, persistent); } catch (final KinesisVideoException e) { // TODO: log/throw } } }; } private boolean isKeyFrame() { return frameIndex % KEY_FRAME_EVERY_60_FRAMES == 0; } private void submitFrameOnUIThread(final KinesisVideoFrame frame) { try { mediaSourceSink.onFrame(frame); } catch (final KinesisVideoException e) { // TODO: log/throw } } @Override public void stop() throws KinesisVideoException { if (bytesGenerator != null) { bytesGenerator.stop(); } mediaSourceState = MediaSourceState.STOPPED; } @Override public boolean isStopped() { return mediaSourceState == MediaSourceState.STOPPED; } @Override public void free() throws KinesisVideoException { } @Override public MediaSourceSink getMediaSourceSink() { return mediaSourceSink; } @Nullable @Override public StreamCallbacks getStreamCallbacks() { return null; } }