diff --git a/webrtc-media/src/main/cpp/include/JNI_AllocationFault.h b/webrtc-media/src/main/cpp/include/JNI_AllocationFault.h index a079a8a2..9b440234 100644 --- a/webrtc-media/src/main/cpp/include/JNI_AllocationFault.h +++ b/webrtc-media/src/main/cpp/include/JNI_AllocationFault.h @@ -30,6 +30,38 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failNextStringCopy (JNIEnv *, jclass); + /* + * Class: dev_onvoid_webrtc_media_player_AllocationFault + * Method: failNextAllocation + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failNextAllocation + (JNIEnv *, jclass); + + /* + * Class: dev_onvoid_webrtc_media_player_AllocationFault + * Method: failNextThreadStart + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failNextThreadStart + (JNIEnv *, jclass); + + /* + * Class: dev_onvoid_webrtc_media_player_AllocationFault + * Method: isArmed + * Signature: ()Z + */ + JNIEXPORT jboolean JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_isArmed + (JNIEnv *, jclass); + + /* + * Class: dev_onvoid_webrtc_media_player_AllocationFault + * Method: throwInNative + * Signature: (ILjava/lang/String;)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_throwInNative + (JNIEnv *, jclass, jint, jstring); + /* * Class: dev_onvoid_webrtc_media_player_AllocationFault * Method: disarm diff --git a/webrtc-media/src/main/cpp/include/media/AllocationFault.h b/webrtc-media/src/main/cpp/include/media/AllocationFault.h index 12a8868e..016966e4 100644 --- a/webrtc-media/src/main/cpp/include/media/AllocationFault.h +++ b/webrtc-media/src/main/cpp/include/media/AllocationFault.h @@ -30,10 +30,21 @@ namespace ffmpeg public: // Fails the next copy of a Java string, once. static void FailNextStringCopy(); + // Fails the next other allocation that is checked, once. + static void FailNextAllocation(); + // Fails the next thread start that is checked, once, as one does + // that the system has no memory for. + static void FailNextThreadStart(); // Forgets a fault that has not fired. static void Disarm(); + // Whether a fault is armed that has not fired. + static bool IsArmed(); // Throws std::bad_alloc if a fault is armed, and disarms it. static void CheckStringCopy(); + // The same for the other allocations. + static void CheckAllocation(); + // Throws std::system_error if a fault is armed, and disarms it. + static void CheckThreadStart(); }; } diff --git a/webrtc-media/src/main/cpp/include/media/MediaRecorder.h b/webrtc-media/src/main/cpp/include/media/MediaRecorder.h index b63ec68c..a198d338 100644 --- a/webrtc-media/src/main/cpp/include/media/MediaRecorder.h +++ b/webrtc-media/src/main/cpp/include/media/MediaRecorder.h @@ -75,7 +75,8 @@ namespace ffmpeg // Start(). int AddTrack(void * frames); - // Attaches to every track and starts writing. + // Attaches to every track and starts writing. A writer thread that + // cannot be started throws, and leaves the recorder unstarted. void Start(); // Detaches from every track, writes out what is queued, finishes @@ -113,6 +114,10 @@ namespace ffmpeg static void OnEncodedFrame(void * opaque, const wj_encoded_frame * frame); void Enqueue(Input * input, const wj_encoded_frame * frame); + + // Makes up for a frame that could not be queued: the video after + // it is useless until the next key frame. + void Dropped(Input * input, bool video); void WantKeyFrameLocked(Input * input); // Makes a video track wait for its next key frame, and asks for diff --git a/webrtc-media/src/main/cpp/src/JNI_AllocationFault.cpp b/webrtc-media/src/main/cpp/src/JNI_AllocationFault.cpp index 527baf2a..eae3d326 100644 --- a/webrtc-media/src/main/cpp/src/JNI_AllocationFault.cpp +++ b/webrtc-media/src/main/cpp/src/JNI_AllocationFault.cpp @@ -18,6 +18,9 @@ #include "media/AllocationFault.h" #include "media/JniBoundary.h" +#include +#include + // The Java class this belongs to exists in the tests only, so that nothing but // them can make an allocation fail. @@ -27,6 +30,51 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failN ffmpeg::AllocationFault::FailNextStringCopy(); } +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failNextAllocation +(JNIEnv * env, jclass caller) +{ + ffmpeg::AllocationFault::FailNextAllocation(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_failNextThreadStart +(JNIEnv * env, jclass caller) +{ + ffmpeg::AllocationFault::FailNextThreadStart(); +} + +JNIEXPORT jboolean JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_isArmed +(JNIEnv * env, jclass caller) +{ + return ffmpeg::AllocationFault::IsArmed() ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_throwInNative +(JNIEnv * env, jclass caller, jint kind, jstring message) +{ + ffmpeg::CallFromJava(env, [&] { + // Held while throwing, as an entry point holds its arguments. + ffmpeg::JavaStringChars text(env, message); + + if (text.get() == nullptr) { + return; + } + + switch (kind) { + case 0: + throw std::bad_alloc(); + case 1: + throw std::runtime_error(text.get()); + case 2: + // A JNI call that failed, and a C++ exception after it. + env->FindClass("dev/onvoid/webrtc/media/player/NoSuchClass"); + + throw std::runtime_error(text.get()); + default: + throw kind; + } + }); +} + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_disarm (JNIEnv * env, jclass caller) { diff --git a/webrtc-media/src/main/cpp/src/JNI_FFmpeg.cpp b/webrtc-media/src/main/cpp/src/JNI_FFmpeg.cpp index e66a1d47..38ae1491 100644 --- a/webrtc-media/src/main/cpp/src/JNI_FFmpeg.cpp +++ b/webrtc-media/src/main/cpp/src/JNI_FFmpeg.cpp @@ -15,6 +15,7 @@ */ #include "JNI_FFmpeg.h" +#include "media/JniBoundary.h" extern "C" { #include @@ -24,14 +25,18 @@ extern "C" { JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_version (JNIEnv * env, jclass caller) { - // The version of the libraries that were actually loaded, which is not - // necessarily the one this module was built against: the LGPL lets an - // application replace them. - return env->NewStringUTF(av_version_info()); + return ffmpeg::CallFromJava(env, nullptr, [&]() -> jstring { + // The version of the libraries that were actually loaded, which is not + // necessarily the one this module was built against: the LGPL lets an + // application replace them. + return env->NewStringUTF(av_version_info()); + }); } JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_player_FFmpeg_license (JNIEnv * env, jclass caller) { - return env->NewStringUTF(avformat_license()); + return ffmpeg::CallFromJava(env, nullptr, [&]() -> jstring { + return env->NewStringUTF(avformat_license()); + }); } diff --git a/webrtc-media/src/main/cpp/src/JNI_MediaPlayer.cpp b/webrtc-media/src/main/cpp/src/JNI_MediaPlayer.cpp index eb8c13e3..13356766 100644 --- a/webrtc-media/src/main/cpp/src/JNI_MediaPlayer.cpp +++ b/webrtc-media/src/main/cpp/src/JNI_MediaPlayer.cpp @@ -37,6 +37,20 @@ namespace { return reinterpret_cast(handle); } + + // Releases a player that never made it to Java. Its observer goes first: + // closing reports the closed state, which would be a call into Java for a + // player nobody listens to yet, and with an exception pending such a call + // is not allowed, and would also clear it. + struct DiscardPlayer + { + void operator()(ffmpeg::MediaPlayer * player) const + { + player->SetObserver(nullptr); + + delete player; + } + }; } JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create @@ -66,9 +80,11 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create return 0; } - auto player = std::make_unique(std::move(reader), api, - reinterpret_cast(videoSourceHandle), - reinterpret_cast(audioSourceHandle)); + // Also released that way when a C++ exception unwinds through here. + std::unique_ptr player( + new ffmpeg::MediaPlayer(std::move(reader), api, + reinterpret_cast(videoSourceHandle), + reinterpret_cast(audioSourceHandle))); player->SetObserver(std::make_unique(env, caller)); player->SetHardwareDecoding(hardwareDecoding == JNI_TRUE); @@ -76,11 +92,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaPlayer_create int result = player->Initialize(); if (result < 0) { - // Released before throwing: closing reports the closed state to the - // observer, which calls into Java, and a call into Java with an - // exception pending is not allowed, and would also clear it. Nothing - // is listening yet anyway, so the observer goes first. - player->SetObserver(nullptr); + // Released before throwing, see DiscardPlayer. player.reset(); ffmpeg::ThrowIOException(env, "Opening the decoders failed: " + ffmpeg::ErrorText(result)); diff --git a/webrtc-media/src/main/cpp/src/media/AllocationFault.cpp b/webrtc-media/src/main/cpp/src/media/AllocationFault.cpp index 7b18864f..abd4fc73 100644 --- a/webrtc-media/src/main/cpp/src/media/AllocationFault.cpp +++ b/webrtc-media/src/main/cpp/src/media/AllocationFault.cpp @@ -18,10 +18,13 @@ #include #include +#include namespace { std::atomic fail_string_copy{ false }; + std::atomic fail_allocation{ false }; + std::atomic fail_thread_start{ false }; } namespace ffmpeg @@ -31,9 +34,26 @@ namespace ffmpeg fail_string_copy.store(true); } + void AllocationFault::FailNextAllocation() + { + fail_allocation.store(true); + } + + void AllocationFault::FailNextThreadStart() + { + fail_thread_start.store(true); + } + void AllocationFault::Disarm() { fail_string_copy.store(false); + fail_allocation.store(false); + fail_thread_start.store(false); + } + + bool AllocationFault::IsArmed() + { + return fail_string_copy.load() || fail_allocation.load() || fail_thread_start.load(); } void AllocationFault::CheckStringCopy() @@ -42,4 +62,19 @@ namespace ffmpeg throw std::bad_alloc(); } } + + void AllocationFault::CheckAllocation() + { + if (fail_allocation.load() && fail_allocation.exchange(false)) { + throw std::bad_alloc(); + } + } + + void AllocationFault::CheckThreadStart() + { + if (fail_thread_start.load() && fail_thread_start.exchange(false)) { + throw std::system_error(std::make_error_code(std::errc::resource_unavailable_try_again), + "Starting a thread failed"); + } + } } diff --git a/webrtc-media/src/main/cpp/src/media/JniBoundary.cpp b/webrtc-media/src/main/cpp/src/media/JniBoundary.cpp index 445260ce..68a179e3 100644 --- a/webrtc-media/src/main/cpp/src/media/JniBoundary.cpp +++ b/webrtc-media/src/main/cpp/src/media/JniBoundary.cpp @@ -18,12 +18,31 @@ #include "media/AllocationFault.h" #include +#include #include #include namespace { std::atomic held_strings{ 0 }; + + // The text of a standard exception is in no defined encoding, and may be + // in the locale's, while ThrowNew reads modified UTF-8. Printable ASCII is + // both, so everything else is replaced. It goes into a buffer of fixed + // size, because this may run when memory has run out. + void CopyAscii(const char * text, char * buffer, size_t size) noexcept + { + size_t length = 0; + + while (text != nullptr && text[length] != '\0' && length + 1 < size) { + const unsigned char c = static_cast(text[length]); + + buffer[length] = (c >= 0x20 && c < 0x7F) ? static_cast(c) : '?'; + length++; + } + + buffer[length] = '\0'; + } } namespace ffmpeg @@ -57,7 +76,11 @@ namespace ffmpeg ThrowJavaException(env, "java/lang/OutOfMemoryError", "Native memory allocation failed"); } catch (const std::exception & e) { - ThrowJavaException(env, "java/lang/RuntimeException", e.what()); + char message[256]; + + CopyAscii(e.what(), message, sizeof(message)); + + ThrowJavaException(env, "java/lang/RuntimeException", message); } catch (...) { ThrowJavaException(env, "java/lang/RuntimeException", "Unknown native exception"); diff --git a/webrtc-media/src/main/cpp/src/media/MediaPlayer.cpp b/webrtc-media/src/main/cpp/src/media/MediaPlayer.cpp index 744d1d45..a44e8765 100644 --- a/webrtc-media/src/main/cpp/src/media/MediaPlayer.cpp +++ b/webrtc-media/src/main/cpp/src/media/MediaPlayer.cpp @@ -15,6 +15,7 @@ */ #include "media/MediaPlayer.h" +#include "media/AllocationFault.h" #include "media/ErrorText.h" #include @@ -91,6 +92,8 @@ namespace ffmpeg { std::lock_guard lock(mutex_); + AllocationFault::CheckThreadStart(); + running_ = true; thread_ = std::thread(&MediaPlayer::Run, this); } diff --git a/webrtc-media/src/main/cpp/src/media/MediaRecorder.cpp b/webrtc-media/src/main/cpp/src/media/MediaRecorder.cpp index d69acdcb..1c728f52 100644 --- a/webrtc-media/src/main/cpp/src/media/MediaRecorder.cpp +++ b/webrtc-media/src/main/cpp/src/media/MediaRecorder.cpp @@ -15,6 +15,7 @@ */ #include "media/MediaRecorder.h" +#include "media/AllocationFault.h" #include "media/ErrorText.h" #include @@ -123,6 +124,8 @@ namespace ffmpeg std::unique_ptr track; try { + AllocationFault::CheckAllocation(); + input = std::make_unique(); track = std::make_unique(index); @@ -161,13 +164,17 @@ namespace ffmpeg } try { + AllocationFault::CheckThreadStart(); + thread_ = std::thread(&MediaRecorder::Run, this); } catch (...) { - // Without a writer nothing may be queued, and stopping only has to - // release the frames and close the file. + // Without a writer the recorder stays as it was before the call: + // it may be started again, and stopping only has to release the + // frames and close the file. std::lock_guard lock(mutex_); + started_ = false; accepting_ = false; throw; } @@ -236,7 +243,16 @@ namespace ffmpeg { Input * input = static_cast(opaque); - input->recorder->Enqueue(input, frame); + // WebRTC calls this through the C interface, on threads of its own, + // and nothing may be thrown back into it. A frame that cannot be + // copied for want of memory is dropped, as is one the writer has no + // room for. + try { + input->recorder->Enqueue(input, frame); + } + catch (...) { + input->recorder->Dropped(input, frame->media_type == WEBRTC_JAVA_MEDIA_VIDEO); + } } void MediaRecorder::Enqueue(Input * input, const wj_encoded_frame * frame) @@ -261,9 +277,11 @@ namespace ffmpeg return; } + // The name first, which may fail to be copied: a track is locked + // only once all of it is set. + input->mime_type = frame->mime_type != nullptr ? frame->mime_type : ""; input->locked = true; input->ssrc = frame->ssrc; - input->mime_type = frame->mime_type != nullptr ? frame->mime_type : ""; } else if (frame->ssrc != input->ssrc) { // Another simulcast layer, which is not recorded; or the stream @@ -333,14 +351,29 @@ namespace ffmpeg recorded.width = frame->width; recorded.height = frame->height; - input->last_time_us = frame->time_us; + // Counted once it is in the queue, which may fail to grow. + AllocationFault::CheckAllocation(); - queued_bytes_ += frame->size; queue_.push_back(std::move(recorded)); + input->last_time_us = frame->time_us; + queued_bytes_ += frame->size; + wake_.notify_one(); } + void MediaRecorder::Dropped(Input * input, bool video) + { + if (!video) { + return; + } + + std::lock_guard lock(mutex_); + + input->waiting_for_key_frame = true; + WantKeyFrameLocked(input); + } + void MediaRecorder::WantKeyFrameLocked(Input * input) { if (!input->key_frame_wanted) { diff --git a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java index 6c5b5a4d..4953cb0e 100644 --- a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java +++ b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/player/MediaFileSource.java @@ -169,7 +169,7 @@ public MediaFileSource(String source, Duration timeout, boolean hardwareDecoding try { player = new MediaPlayer(reader, videoSource, audioSource, hardwareDecoding); } - catch (IOException | RuntimeException e) { + catch (IOException | RuntimeException | Error e) { // The player did not take charge, so what was made here has to be // unwound rather than left to leak. disposeSources(); diff --git a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/recorder/MediaRecorder.java b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/recorder/MediaRecorder.java index f3df8cc7..f164b12b 100644 --- a/webrtc-media/src/main/java/dev/onvoid/webrtc/media/recorder/MediaRecorder.java +++ b/webrtc-media/src/main/java/dev/onvoid/webrtc/media/recorder/MediaRecorder.java @@ -232,9 +232,19 @@ public void start() { return thread; }); - state = MediaRecorderState.RECORDING; + try { + start(handle); + } + catch (RuntimeException | Error e) { + // The native recorder could not start its writer, and is as + // it was: still idle, and closing it deletes its file. + events.shutdown(); + events = null; - start(handle); + throw e; + } + + state = MediaRecorderState.RECORDING; } } diff --git a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/AllocationFault.java b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/AllocationFault.java index f4a94180..f9a8a05d 100644 --- a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/AllocationFault.java +++ b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/AllocationFault.java @@ -41,6 +41,35 @@ private AllocationFault() { */ public static native void failNextStringCopy(); + /** + * Fails the next other native allocation that is checked, such as that of + * a recorder track, or the copy of a frame to record. + */ + public static native void failNextAllocation(); + + /** + * Fails the next native thread start that is checked, such as that of a + * player's decoder or a recorder's writer, as one fails that the system + * has no memory for. + */ + public static native void failNextThreadStart(); + + /** + * @return True if a fault is armed that has not fired yet. + */ + public static native boolean isArmed(); + + /** + * Throws a C++ exception in native code that holds the given string, for + * the entry point to turn into a Java exception. + * + * @param kind 0 for std::bad_alloc, 1 for a std::runtime_error with the + * message, 2 for the same after a JNI call that failed, + * anything else for one that is no std::exception. + * @param message The message. + */ + static native void throwInNative(int kind, String message); + /** * Forgets a fault that has not fired. */ diff --git a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/NativeBoundaryTest.java b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/NativeBoundaryTest.java new file mode 100644 index 00000000..57e5b574 --- /dev/null +++ b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/player/NativeBoundaryTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Alex Andres + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.onvoid.webrtc.media.player; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; + +import dev.onvoid.webrtc.media.audio.CustomAudioSource; +import dev.onvoid.webrtc.media.video.CustomVideoSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tests that a C++ exception in the native library reaches Java as a Java + * exception rather than ending the process, with nothing it held left behind. + */ +@Execution(ExecutionMode.SAME_THREAD) +class NativeBoundaryTest { + + private static final String ASSET = "/media-test.webm"; + + + @AfterEach + void disarm() { + AllocationFault.disarm(); + } + + @Test + void translatesNativeExceptions() { + OutOfMemoryError memory = assertThrows(OutOfMemoryError.class, + () -> AllocationFault.throwInNative(0, "unused")); + assertEquals("Native memory allocation failed", memory.getMessage()); + + // The text of a standard exception is in no defined encoding, so only + // its ASCII reaches Java. + RuntimeException runtime = assertThrows(RuntimeException.class, + () -> AllocationFault.throwInNative(1, "Fehler: ΓΌ")); + assertEquals("Fehler: ??", runtime.getMessage()); + + // The exception of the JNI call that failed is the one Java sees. + assertThrows(NoClassDefFoundError.class, () -> AllocationFault.throwInNative(2, "unused")); + + RuntimeException unknown = assertThrows(RuntimeException.class, + () -> AllocationFault.throwInNative(3, "unused")); + assertEquals("Unknown native exception", unknown.getMessage()); + + assertEquals(0, AllocationFault.heldStrings()); + } + + @Test + void playerFailingToStartReleasesReader() throws Exception { + Path asset = MediaReaderTest.asset(ASSET); + MediaReader reader = new MediaReader(asset); + CustomVideoSource videoSource = new CustomVideoSource(); + CustomAudioSource audioSource = new CustomAudioSource(); + + try { + AllocationFault.failNextThreadStart(); + + RuntimeException e = assertThrows(RuntimeException.class, + () -> new MediaPlayer(reader, videoSource, audioSource)); + assertTrue(e.getMessage().startsWith("Starting a thread failed"), e.getMessage()); + + // Taken over by the player, and released along with it. + assertThrows(IllegalStateException.class, reader::getInfo); + + try (MediaPlayer player = new MediaPlayer(new MediaReader(asset), + videoSource, audioSource)) { + assertEquals(MediaPlayerState.IDLE, player.getState()); + } + } + finally { + reader.close(); + videoSource.dispose(); + audioSource.dispose(); + } + } + +} diff --git a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/recorder/MediaRecorderTest.java b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/recorder/MediaRecorderTest.java index e87f3ef8..03d2576f 100644 --- a/webrtc-media/src/test/java/dev/onvoid/webrtc/media/recorder/MediaRecorderTest.java +++ b/webrtc-media/src/test/java/dev/onvoid/webrtc/media/recorder/MediaRecorderTest.java @@ -285,6 +285,103 @@ public void onStarted() { assertTrue(readInfo(file).hasVideo()); } + @Test + void failedAddTrackLeavesRecorderAsItWas() throws Exception { + Path file = tempDir.resolve("failed-track.mkv"); + Listener listener = new Listener(); + + try (TestCall call = new TestCall(factory); + MediaRecorder recorder = new MediaRecorder(file)) { + recorder.setListener(listener); + + AllocationFault.failNextAllocation(); + + assertThrows(OutOfMemoryError.class, () -> recorder.addTrack(call.getVideoSender())); + + // Not added on either side: there is nothing to start with. + assertThrows(IllegalStateException.class, recorder::start); + + recorder.addTrack(call.getVideoSender()); + recorder.start(); + + assertTrue(listener.started.await(10, TimeUnit.SECONDS), "not started"); + assertTrue(recorder.stop()); + } + finally { + AllocationFault.disarm(); + } + + assertNull(listener.error.get()); + assertTrue(readInfo(file).hasVideo()); + } + + @Test + void failedStartLeavesRecorderIdle() throws Exception { + Path file = tempDir.resolve("failed-start.mkv"); + Listener listener = new Listener(); + + try (TestCall call = new TestCall(factory); + MediaRecorder recorder = new MediaRecorder(file)) { + recorder.setListener(listener); + recorder.addTrack(call.getVideoSender()); + + AllocationFault.failNextThreadStart(); + + RuntimeException e = assertThrows(RuntimeException.class, recorder::start); + assertTrue(e.getMessage().startsWith("Starting a thread failed"), e.getMessage()); + assertEquals(MediaRecorderState.IDLE, recorder.getState()); + + // Nothing was left half started, so it starts on a second try. + recorder.start(); + + assertTrue(listener.started.await(10, TimeUnit.SECONDS), "not started"); + assertTrue(recorder.stop()); + } + finally { + AllocationFault.disarm(); + } + + assertNull(listener.error.get()); + assertTrue(readInfo(file).hasVideo()); + } + + @Test + void dropsFrameThatCannotBeQueued() throws Exception { + Path file = tempDir.resolve("dropped-frame.mkv"); + Listener listener = new Listener(); + + try (TestCall call = new TestCall(factory); + MediaRecorder recorder = new MediaRecorder(file)) { + recorder.setListener(listener); + recorder.addTrack(call.getVideoSender()); + recorder.start(); + + assertTrue(listener.started.await(10, TimeUnit.SECONDS), "not started"); + + // The next frame cannot be copied, on a WebRTC thread, where + // nothing may be thrown. + AllocationFault.failNextAllocation(); + + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + + while (AllocationFault.isArmed() && System.nanoTime() < deadline) { + Thread.sleep(10); + } + + assertFalse(AllocationFault.isArmed(), "no frame arrived"); + + Thread.sleep(1000); + + assertTrue(recorder.stop()); + } + finally { + AllocationFault.disarm(); + } + + assertNull(listener.error.get()); + assertTrue(readInfo(file).hasVideo()); + } + /** * Records a call that sends G.711 into Matroska, which has no codec ID of * its own for it, and reads the audio back as the codec it was sent in.