Skip to content

Commit 67abd8e

Browse files
author
Yuqi Huang
committed
Merge branch 'develop' into add-event-metadata-support
1 parent 7d37263 commit 67abd8e

File tree

17 files changed

+1157
-79
lines changed

17 files changed

+1157
-79
lines changed

jni/com/amazonaws/kinesis/video/producer/jni/KinesisVideoClientWrapper.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,30 @@ KinesisVideoClientWrapper::KinesisVideoClientWrapper(JNIEnv* env,
3333
if (env->GetJavaVM(&mJVMContext.jvm) != 0 || mJVMContext.jvm == NULL) {
3434
CHECK_EXT(FALSE, "Couldn't retrieve the JavaVM reference.");
3535
}
36-
clientRegistryInfo = ClientRegistry::getInstance().addClient(this); // Note: 'this' cannot be null
37-
mJVMContext.clientId = clientRegistryInfo.second;
3836

3937
// Set the callbacks
4038
if (!setCallbacks(env, thiz, isFirstClient)) {
4139
throwNativeException(env, EXCEPTION_NAME, "Failed to set the callbacks.", STATUS_INVALID_ARG);
42-
ClientRegistry::getInstance().removeClient(this);
4340
return;
4441
}
4542

4643
// Extract the DeviceInfo structure
4744
MEMSET(&mDeviceInfo, 0x00, SIZEOF(DeviceInfo));
4845
if (!setDeviceInfo(env, deviceInfo, &mDeviceInfo)) {
4946
throwNativeException(env, EXCEPTION_NAME, "Failed to set the DeviceInfo structure.", STATUS_INVALID_ARG);
50-
ClientRegistry::getInstance().removeClient(this);
5147
return;
5248
}
5349

5450
// Creating the client object might return an error as well so freeing potentially allocated tags right after the call.
5551
retStatus = createKinesisVideoClient(&mDeviceInfo, &mClientCallbacks, &mClientHandle);
5652
if (STATUS_SUCCEEDED(retStatus) && isFirstClient) {
5753
firstClientCreated = true;
54+
clientRegistryInfo = ClientRegistry::getInstance().addClient(this); // Note: 'this' cannot be null
55+
mJVMContext.clientId = clientRegistryInfo.second;
5856
}
5957
releaseTags(mDeviceInfo.tags);
6058
if (STATUS_FAILED(retStatus)) {
6159
throwNativeException(env, EXCEPTION_NAME, "Failed to create Kinesis Video client.", retStatus);
62-
ClientRegistry::getInstance().removeClient(this);
6360
return;
6461
}
6562

src/main/java/com/amazonaws/kinesisvideo/client/PutMediaClient.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ private void sign(final ParallelSimpleHttpClient client) {
105105

106106
private Consumer<OutputStream> sendChunkEncodedMvkStream(final int fragmentThrottle) {
107107
return new Consumer<OutputStream>() {
108+
109+
// Note: The thread name should already have the streamName + uploadHandle
108110
@Override
109111
public void accept(final OutputStream rawOutputStream) {
110112
FileOutputStream outputFileStream = null;
@@ -136,10 +138,20 @@ public void accept(final OutputStream rawOutputStream) {
136138
rawOutputStream.flush();
137139
log.debug("Data sent. counter: {}", counter);
138140
} catch (final Exception e) {
139-
log.debug("Exception while sending data.", e);
140-
throw new RuntimeException("Exception while sending encoded chunk in MKV stream ! ", e);
141+
log.debug(mBuilder.mStreamName + ": Exception while sending data.", e);
142+
throw new RuntimeException(mBuilder.mStreamName + ": Exception while sending encoded chunk in MKV stream ! ", e);
141143
} finally {
142144
tryCloseOutputFileStream(outputFileStream);
145+
146+
if (mBuilder.mMkvStream != null) {
147+
try {
148+
log.trace("Closing mkv stream");
149+
mBuilder.mMkvStream.close();
150+
log.trace("Closed the mkv stream");
151+
} catch (final IOException e) {
152+
log.error("Failed to close the MKV stream", e);
153+
}
154+
}
143155
}
144156
}
145157
};

src/main/java/com/amazonaws/kinesisvideo/http/ParallelSimpleHttpClient.java

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import com.amazonaws.kinesisvideo.client.IPVersionFilter;
44
import com.amazonaws.kinesisvideo.client.KinesisVideoClientConfigurationDefaults;
55
import com.amazonaws.kinesisvideo.common.function.Consumer;
6+
import com.amazonaws.kinesisvideo.common.preconditions.Preconditions;
67
import com.amazonaws.kinesisvideo.socket.SocketFactory;
78
import com.amazonaws.kinesisvideo.util.LoggedExitRunnable;
89
import com.google.common.util.concurrent.ThreadFactoryBuilder;
910
import org.apache.logging.log4j.LogManager;
1011
import org.apache.logging.log4j.Logger;
1112

13+
import javax.annotation.Nonnull;
14+
import javax.annotation.Nullable;
1215
import java.io.BufferedWriter;
1316
import java.io.IOException;
1417
import java.io.InputStream;
@@ -18,15 +21,21 @@
1821
import java.net.Socket;
1922
import java.net.URI;
2023
import java.nio.charset.Charset;
24+
import java.util.ArrayList;
2125
import java.util.HashMap;
26+
import java.util.List;
2227
import java.util.Map;
2328
import java.util.UUID;
2429
import java.util.concurrent.ExecutorService;
2530
import java.util.concurrent.Executors;
31+
import java.util.concurrent.TimeUnit;
2632

2733
import static com.amazonaws.kinesisvideo.common.preconditions.Preconditions.checkNotNull;
2834

2935
public final class ParallelSimpleHttpClient implements HttpClient {
36+
37+
private static final int AWAIT_THREAD_TERMINATE_SECS = 3;
38+
3039
private static final String SPACE = " ";
3140
private static final String CLRF = "\r\n";
3241
private static final String HTTP_1_1 = "HTTP/1.1";
@@ -51,6 +60,34 @@ public void accept(final Exception object) {
5160
private OutputStream mOutputStream;
5261
private ExecutorService payloadSender;
5362
private ExecutorService responseReceiver;
63+
private final List<ExitResult> exitHistory = new ArrayList<>();
64+
65+
private enum Caller {
66+
SENDER,
67+
RECEIVER,
68+
CLOSE
69+
}
70+
71+
private static class ExitResult {
72+
@Nonnull
73+
private Caller caller;
74+
75+
@Nullable
76+
private Exception exception;
77+
78+
ExitResult(@Nonnull final Caller caller, @Nullable final Exception exception) {
79+
this.caller = caller;
80+
this.exception = exception;
81+
}
82+
83+
@Override
84+
public String toString() {
85+
return "ExitResult{" +
86+
"caller=" + caller +
87+
", exception=" + exception +
88+
'}';
89+
}
90+
}
5491

5592
private ParallelSimpleHttpClient(final Builder builder) {
5693
mBuilder = builder;
@@ -157,10 +194,7 @@ public void execute() {
157194
log.error("[{}] Exception thrown on sending thread", mBuilder.mStreamName, e);
158195
storedException = e;
159196
} finally {
160-
//Only call completion if there is an exception, otherwise sender will call completion
161-
if (storedException != null) {
162-
mBuilder.mCompletion.accept(storedException);
163-
}
197+
notifyCompletionCallback(new ExitResult(Caller.SENDER, storedException));
164198
payloadSender.shutdownNow();
165199
}
166200
}
@@ -184,7 +218,7 @@ public void execute() {
184218
log.error("[{}] Exception thrown on receiving thread", mBuilder.mStreamName, e);
185219
storedException = e;
186220
} finally {
187-
mBuilder.mCompletion.accept(storedException);
221+
notifyCompletionCallback(new ExitResult(Caller.RECEIVER, storedException));
188222
responseReceiver.shutdownNow();
189223
closeSocket();
190224
}
@@ -245,7 +279,83 @@ public void close() throws IOException {
245279
payloadSender.shutdownNow();
246280
responseReceiver.shutdownNow();
247281
closeSocket();
248-
mBuilder.mCompletion.accept(null);
282+
283+
awaitTryShutdownThreads();
284+
285+
notifyCompletionCallback(new ExitResult(Caller.CLOSE, null));
286+
}
287+
288+
// This is used to synchronize the 3 threads which call the completion callback:
289+
// - Sender thread
290+
// - Receiving ACKs thread
291+
// - Thread calling close()
292+
// If close() is called, it will immediately invoke the completion callback with success.
293+
// Otherwise, it will wait for both sender and receiver threads to exit before notifying.
294+
// If applicable, the thread that threw the exception first's result will be propagated.
295+
private void notifyCompletionCallback(@Nonnull final ExitResult exitResult) {
296+
// Note: the thread name should already have the stream name + connection handle # in it
297+
log.debug("Received: {}", exitResult);
298+
299+
if (mBuilder.mCompletion != null) {
300+
301+
if (exitResult.caller == Caller.CLOSE) {
302+
mBuilder.mCompletion.accept(null);
303+
return;
304+
}
305+
306+
Exception exceptionToNotify = null;
307+
boolean notify = false;
308+
synchronized (this.exitHistory) {
309+
this.exitHistory.add(exitResult);
310+
311+
if (this.exitHistory.size() == 2 &&
312+
((this.exitHistory.get(0).caller == Caller.SENDER && this.exitHistory.get(1).caller == Caller.RECEIVER) ||
313+
(this.exitHistory.get(0).caller == Caller.RECEIVER && this.exitHistory.get(1).caller == Caller.SENDER))
314+
) {
315+
// Check if either one of them exited with an exception
316+
// If so, propagate it. If both of them terminated normally, notify with null
317+
notify = true;
318+
319+
// prioritize the exception that came first
320+
exceptionToNotify = this.exitHistory.get(0).exception;
321+
if (exceptionToNotify == null) {
322+
exceptionToNotify = this.exitHistory.get(1).exception;
323+
}
324+
} else {
325+
log.debug("Not notifying this time, caller history: {}", this.exitHistory);
326+
}
327+
}
328+
329+
if (notify) {
330+
log.debug("[{}] notifying completion callback with {}", mBuilder.mStreamName, exceptionToNotify);
331+
mBuilder.mCompletion.accept(exceptionToNotify);
332+
}
333+
}
334+
}
335+
336+
// Wait for the threads to terminate
337+
// If the threads are not alive, returns immediately
338+
// Expecting these to be near instantaneous
339+
private void awaitTryShutdownThreads() {
340+
awaitTermination(this.payloadSender, "payload sender", AWAIT_THREAD_TERMINATE_SECS);
341+
awaitTermination(this.responseReceiver, "response receiver", AWAIT_THREAD_TERMINATE_SECS);
342+
}
343+
344+
@SuppressWarnings("ConstantConditions")
345+
private void awaitTermination(@Nonnull final ExecutorService executor, @Nonnull final String id,
346+
final int threadTerminateTimeoutSeconds) {
347+
Preconditions.checkArgument(executor != null, "Executor cannot be null");
348+
Preconditions.checkArgument(id != null, "ID cannot be null");
349+
Preconditions.checkArgument(threadTerminateTimeoutSeconds >= 0, "ThreadTerminateTimeoutSeconds must be positive");
350+
351+
try {
352+
if (!executor.awaitTermination(AWAIT_THREAD_TERMINATE_SECS, TimeUnit.SECONDS)) {
353+
log.error("{}: {} couldn't shutdown within {} seconds", mBuilder.mStreamName, id, AWAIT_THREAD_TERMINATE_SECS);
354+
}
355+
} catch (final InterruptedException e) {
356+
log.error("{}: Interrupted while waiting for {} shutdown", mBuilder.mStreamName, id, e);
357+
Thread.currentThread().interrupt();
358+
}
249359
}
250360

251361

src/main/java/com/amazonaws/kinesisvideo/internal/producer/jni/NativeKinesisVideoProducerStream.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Set;
2626
import java.util.concurrent.CountDownLatch;
2727
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicBoolean;
2829

2930
/**
3031
* Implementation of {@link KinesisVideoProducerStream}
@@ -38,7 +39,7 @@ private class NativeDataInputStream extends InputStream {
3839
/**
3940
* Whether the stream has been closed
4041
*/
41-
private volatile boolean mStreamClosed = false;
42+
private AtomicBoolean mStreamClosed = new AtomicBoolean(false);
4243

4344
// Set the notification values
4445
private final Object mMonitor = new Object();
@@ -63,7 +64,7 @@ public int read(final byte[] b,
6364
final int off,
6465
final int len)
6566
throws IOException {
66-
if (mStreamClosed) {
67+
if (mStreamClosed.get()) {
6768
mLog.warn("Stream {} with uploadHandle {} has been closed", mStreamInfo.getName(), mUploadHandle);
6869
}
6970

@@ -72,9 +73,9 @@ public int read(final byte[] b,
7273
// is handled by simply spin-locking until the data is available.
7374
int bytesRead = -1;
7475

75-
while (!mStreamClosed) {
76+
while (!mStreamClosed.get()) {
7677
synchronized (mMonitor) {
77-
while (!mDataAvailable && !mStreamClosed) {
78+
while (!mDataAvailable && !mStreamClosed.get()) {
7879
try {
7980
mLog.debug("no data for stream {} with uploadHandle {}, waiting", mStreamInfo.getName(),
8081
mUploadHandle);
@@ -87,7 +88,7 @@ public int read(final byte[] b,
8788

8889
// Clear the availability indicator for now
8990
mDataAvailable = false;
90-
if (mStreamClosed) {
91+
if (mStreamClosed.get()) {
9192
// Indicate the EOS
9293
bytesRead = -1;
9394
mLog.debug("Being notified to close stream {} with uploadHandle {}",
@@ -108,7 +109,7 @@ public int read(final byte[] b,
108109
mStreamInfo.getName(), mUploadHandle);
109110

110111
// Set the flag so the stream is not valid any longer
111-
mStreamClosed = true;
112+
mStreamClosed.set(true);
112113

113114
if (0 == bytesRead) {
114115
// Indicate the EOS
@@ -154,11 +155,11 @@ public int read(final byte[] b)
154155
public void close()
155156
throws IOException
156157
{
157-
// Set the stream to stopped state
158-
mStreamClosed = true;
159-
160-
// Notify the awaiting thread
161-
notifyReaderThread(0, 0);
158+
// Set the stream to stopped state only once
159+
if (mStreamClosed.compareAndSet(false, true)) {
160+
// Notify the awaiting thread
161+
notifyReaderThread(0, 0);
162+
}
162163
}
163164

164165
protected void notifyReaderThread(final long duration, final long availableSize) {
@@ -178,7 +179,7 @@ protected void endOfReaderThread() {
178179
// Unblock the awaiting reading code block
179180
synchronized (mMonitor) {
180181
mDataAvailable = true;
181-
mStreamClosed = true;
182+
mStreamClosed.set(true);
182183
mMonitor.notify();
183184
}
184185
}

src/main/java/com/amazonaws/kinesisvideo/internal/service/AckConsumer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class AckConsumer implements Consumer<InputStream> {
2525
private final Logger log;
2626
private final long uploadHandle;
2727
private volatile boolean closed = false;
28+
private volatile boolean seenErrorAck = false;
2829

2930
public AckConsumer(final long uploadHandle,
3031
@Nonnull final KinesisVideoProducerStream stream,
@@ -72,6 +73,11 @@ private void processAckInputStream() {
7273
closed = true;
7374
} else if (bytesRead != 0) {
7475
log.debug("Received ACK bits: {}", bytesString);
76+
77+
if (bytesString.contains("ERROR")) {
78+
seenErrorAck = true;
79+
}
80+
7581
try {
7682
stream.parseFragmentAck(uploadHandle, bytesString);
7783
} catch (final ProducerException e) {
@@ -90,6 +96,10 @@ private void processAckInputStream() {
9096
}
9197
}
9298

99+
public boolean seenErrorAck() {
100+
return this.seenErrorAck;
101+
}
102+
93103
public void close() throws ProducerException {
94104
// Trigger stopping
95105
closed = true;

0 commit comments

Comments
 (0)