feat(#430): Add Debug overlay for video playback.#511
Conversation
Adds a Debug Overlay for video playback that contains general informat for the state of playback. Also fixes an issue with the app crashing when the user login can not connect to the server.
WalkthroughWraps login repository calls to return Result.Error on exceptions; adds showError() to login view contract, activity UI and retry handling; introduces ExoPlayer debug overlay, debug info propagation, transcoding tracking and a debug toggle; replaces playback control layout; and expands unit/UI tests for these flows. Changes
Sequence Diagram(s)sequenceDiagram
participant Presenter
participant Activity as View
participant Player
participant TranscodeService as Transcode/Server
Presenter->>Presenter: playVideo(video)
Presenter->>TranscodeService: isDirectPlaySupportedForContainer(video)
alt direct play supported
Presenter->>Presenter: isTranscoding = false
Presenter->>Player: init with directUrl
else transcoding required
Presenter->>TranscodeService: createTranscodeUrl(video)
TranscodeService-->>Presenter: transcodeUrl
Presenter->>Presenter: isTranscoding = true
Presenter->>Player: init with transcodeUrl
end
Presenter->>Activity: updateSerenityDebugInfo(isTranscoding, videoCodec, audioCodec, bitrate)
Presenter->>Player: startPlayback()
Player-->>Activity: track/bitrate updates
Activity->>Activity: toggle debug overlay (toggleDebugView)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #511 +/- ##
=============================================
+ Coverage 46.68% 48.61% +1.93%
- Complexity 456 473 +17
=============================================
Files 65 65
Lines 2155 2199 +44
Branches 300 309 +9
=============================================
+ Hits 1006 1069 +63
+ Misses 1032 1002 -30
- Partials 117 128 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserActivityTest.kt (2)
43-52: setUp order should callsuper.setUp()first.Per coding guidelines,
setUpshould callsuper.setUp()as the first action. CurrentlyclearAllMocks()is called beforesuper.setUp().Suggested fix
`@Before` override fun setUp() { - clearAllMocks() super.setUp() + clearAllMocks() val intent = Intent()Based on coding guidelines: "Extend
us.nineworlds.serenity.test.InjectingTestfor Toothpick-based tests and overridesetUpwith a call tosuper.setUp()as the first action"
54-57: AddclearAllMocks()to tearDown.Per coding guidelines, mocks should be cleared in
@Afterblocks usingclearAllMocks().Suggested fix
`@After` fun tearDown() { + clearAllMocks() activity.finish() }Based on coding guidelines: "Define setup and cleanup routines explicitly in unit tests, ensuring all mocks are cleared in
@Afterblocks usingclearAllMocks()andToothpick.reset()"serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenterTest.kt (1)
67-76: setUp order should callsuper.setUp()first.Per coding guidelines,
setUpshould callsuper.setUp()as the first action. CurrentlyDispatchers.setMain()andclearAllMocks()are called beforesuper.setUp().Suggested fix
`@Before` override fun setUp() { + super.setUp() Dispatchers.setMain(Dispatchers.Unconfined) clearAllMocks() - super.setUp() presenter = spyk(ExoplayerPresenter())Based on coding guidelines: "override
setUpwith a call tosuper.setUp()as the first action"serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt (1)
46-57: Callsuper.setUp()first and move mock cleanup to@After.The
super.setUp()must be the first statement in the override to properly initialize the Toothpick scope before any presenter creation. Additionally, the@Afterblock is incomplete—it must clear all mocks and reset Toothpick to prevent scope leakage across tests. Importtoothpick.Toothpickif not already present.♻️ Proposed fix
`@Before` override fun setUp() { - clearAllMocks() - super.setUp() - presenter = LoginUserPresenter() + super.setUp() Dispatchers.setMain(UnconfinedTestDispatcher()) + presenter = LoginUserPresenter() } `@After` fun tearDown() { Dispatchers.resetMain() + clearAllMocks() + Toothpick.reset() }serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (1)
106-129: Use View Binding properties for all Activity views instead of mixing withfindViewById.The Activity initializes
ActivityExoplayerVideoBindingand already accessesbinding.playerViewandbinding.root, but still usesfindViewById()for debug overlay views (exo_debug_layout,exo_debug_text_view,serenity_debug_text_view). Access these views through the binding object instead for consistency and type safety. Replace the threefindViewByIdcalls at lines 127–129 and line 223 with binding properties:- dataLoadingContainer = findViewById(R.id.data_loading_container) - exoDebugLayout = findViewById(R.id.exo_debug_layout) - serenityDebugTextView = findViewById(R.id.serenity_debug_text_view) + dataLoadingContainer = binding.dataLoadingContainer + exoDebugLayout = binding.exoDebugLayout + serenityDebugTextView = binding.serenityDebugTextView- debugTextViewHelper = DebugTextViewHelper(player, findViewById(R.id.exo_debug_text_view)) + debugTextViewHelper = DebugTextViewHelper(player, binding.exoDebugTextView)Also applies to: 223, 402, 406–409
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt (1)
85-105: Callsuper.setUp()first in thesetUp()override.The base
InjectingTestclass must initialize scopes and inject dependencies before other setup operations. MoveclearAllMocks()aftersuper.setUp()to comply with the initialization order requirement.♻️ Proposed fix
`@Before` override fun setUp() { - clearAllMocks() - super.setUp() + super.setUp() + clearAllMocks() mockkConstructor(VideoKeyCodeHandlerDelegate::class)
🤖 Fix all issues with AI agents
In `@serenity-app/src/main/res/layout-large/activity_login_user.xml`:
- Around line 41-47: The Button with id retry_button in activity_login_user.xml
uses a hardcoded label "Retry"; add a string resource named retry in
res/values/strings.xml (e.g., <string name="retry">Retry</string>) and update
the Button's android:text to reference it (android:text="@string/retry") to
enable proper i18n.
In `@serenity-app/src/main/res/layout/exo_player_control_view.xml`:
- Around line 96-102: Replace the hardcoded contentDescription on the
ImageButton with a string resource: add a new entry named toggle_debug_info in
res/values/strings.xml (value "Toggle Debug Info") and update the ImageButton
with id @+id/exo_debug_toggle to reference `@string/toggle_debug_info` for its
contentDescription attribute so the UI text is localized.
🧹 Nitpick comments (3)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginRepositoryTest.kt (1)
40-43: AddclearAllMocks()andToothpick.reset()to the tearDown method.Per coding guidelines, cleanup routines should include
clearAllMocks()andToothpick.reset()in@Afterblocks. Currently,clearAllMocks()is only in@Before.Suggested fix
`@After` fun tearDown() { + clearAllMocks() + Toothpick.reset() Dispatchers.resetMain() }Add the import:
import toothpick.ToothpickBased on coding guidelines, cleanup routines should ensure all mocks are cleared in
@Afterblocks usingclearAllMocks()andToothpick.reset().serenity-app/src/main/res/layout/exo_player_control_view.xml (1)
60-84: UsepaddingStart/paddingEndinstead ofpaddingLeft/paddingRightfor RTL support.The position and duration TextViews use
paddingLeft/paddingRightwhich don't adapt to right-to-left layouts.Suggested fix
<TextView android:id="@id/exo_position" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textStyle="bold" - android:paddingLeft="4dp" - android:paddingRight="4dp" + android:paddingStart="4dp" + android:paddingEnd="4dp" android:includeFontPadding="false" android:textColor="#FFBEBEBE"/><TextView android:id="@id/exo_duration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" android:textStyle="bold" - android:paddingLeft="4dp" - android:paddingRight="4dp" + android:paddingStart="4dp" + android:paddingEnd="4dp" android:includeFontPadding="false" android:textColor="#FFBEBEBE"/>serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (1)
80-85: PrefermoxyPresenterdelegate over@InjectPresenter.Use Provider +
moxyPresenterand remove@InjectPresenter/@ProvidePresenterto align with the required Moxy pattern. As per coding guidelines, please use the delegate‑based injection.♻️ Proposed refactor
-import moxy.presenter.InjectPresenter -import moxy.presenter.ProvidePresenter +import moxy.ktx.moxyPresenter @@ - `@InjectPresenter` - lateinit var presenter: ExoplayerPresenter + internal val presenter by moxyPresenter { presenterProvider.get() } @@ - `@ProvidePresenter` - fun providePresenter(): ExoplayerPresenter = presenterProvider.get()Also applies to: 112-114
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt (1)
46-52: Callsuper.setUp()before any test setup logic.
InjectingTestrequiressuper.setUp()as the first action; moveclearAllMocks()below it to keep Toothpick setup consistent.🛠️ Proposed fix
`@Before` override fun setUp() { - clearAllMocks() - super.setUp() + super.setUp() + clearAllMocks() presenter = LoginUserPresenter() Dispatchers.setMain(UnconfinedTestDispatcher()) }As per coding guidelines, ensure
super.setUp()is the first action.serenity-app/src/main/kotlin/us/nineworlds/serenity/core/repository/SettingsRepository.kt (1)
76-85: Keepremove()consistent with the broadenedcontains()check.
contains()now checks long/float/double keys, butremove()only deletes string/boolean/int. Keys of other types will persist andcontains()may keep returning true after removal attempts.🔧 Proposed fix
fun remove(key: String) = runBlocking { val stringKey = stringPreferencesKey(key) val booleanKey = booleanPreferencesKey(key) val intKey = intPreferencesKey(key) + val longKey = longPreferencesKey(key) + val floatKey = floatPreferencesKey(key) + val doubleKey = doublePreferencesKey(key) context.dataStore.edit { preferences -> preferences.remove(stringKey) preferences.remove(booleanKey) preferences.remove(intKey) + preferences.remove(longKey) + preferences.remove(floatKey) + preferences.remove(doubleKey) } Unit }serenity-app/src/test/kotlin/us/nineworlds/serenity/core/repository/SettingsRepositoryTest.kt (1)
33-48: Align test lifecycle with MockK/Toothpick guidelines.
super.setUp()must be the first call insetUp(), before any mock setup.- Static mocks (
mockkStatic) belong in@BeforeClass/@AfterClass, not in instance-level@Before.Toothpick.reset()must be explicitly called in the test's@Afterblock.Suggested adjustment
+import io.mockk.unmockkStatic +import org.junit.AfterClass +import org.junit.BeforeClass +import toothpick.Toothpick + `@Ignore` class SettingsRepositoryTest : InjectingTest() { + companion object { + `@JvmStatic` + `@BeforeClass` + fun setUpClass() { + mockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt") + mockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt") + } + + `@JvmStatic` + `@AfterClass` + fun tearDownClass() { + unmockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt") + unmockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt") + } + } + `@Before` override fun setUp() { - mockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt") - mockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt") + super.setUp() every { mockContext.dataStore } returns mockDataStore every { mockDataStore.data } returns flowOf(mockPreferences) - - super.setUp() repository = scope.getInstance(SettingsRepository::class.java) } `@After` fun tearDown() { clearAllMocks() closeScope() + Toothpick.reset() }serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt (1)
85-115: Move constructor mocks to@BeforeClass/@afterclass and callsuper.setUp()first.Constructor mocks are global state and must live in class-level setup/teardown. The
super.setUp()call must be the first statement in the instancesetUp()method.♻️ Suggested change
companion object { private val mockExoPlayerPresenter = mockk<ExoplayerPresenter>(relaxed = true) private val mockDataSourceFactory = mockk<DataSource.Factory>(relaxed = true) private val mockTrackSelector = mockk<TrackSelector>(relaxed = true) private val mockLogger = mockk<Logger>(relaxed = true) private val mockPlayer = mockk<ExoPlayer>(relaxed = true) private val mockTimeUtil = mockk<TimeUtil>(relaxed = true) private val mockSharedPreferences = mockk<SharedPreferences>(relaxed = true) + + `@JvmStatic` + `@BeforeClass` + fun setUpClass() { + mockkConstructor(VideoKeyCodeHandlerDelegate::class) + mockkConstructor(DefaultLoadControl.Builder::class) + mockkConstructor(DefaultRenderersFactory::class) + mockkConstructor(ExoPlayer.Builder::class) + } + + `@JvmStatic` + `@AfterClass` + fun tearDownClass() { + unmockkConstructor(VideoKeyCodeHandlerDelegate::class) + unmockkConstructor(DefaultLoadControl.Builder::class) + unmockkConstructor(DefaultRenderersFactory::class) + unmockkConstructor(ExoPlayer.Builder::class) + } } `@Before` override fun setUp() { - clearAllMocks() super.setUp() - mockkConstructor(VideoKeyCodeHandlerDelegate::class) - mockkConstructor(DefaultLoadControl.Builder::class) - mockkConstructor(DefaultRenderersFactory::class) - mockkConstructor(ExoPlayer.Builder::class) + clearAllMocks() val mockBuilder = mockk<ExoPlayer.Builder>(relaxed = true) `@After` fun tearDown() { clearAllMocks() - unmockkConstructor(VideoKeyCodeHandlerDelegate::class) - unmockkConstructor(DefaultLoadControl.Builder::class) - unmockkConstructor(DefaultRenderersFactory::class) - unmockkConstructor(ExoPlayer.Builder::class) Toothpick.reset() }serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (2)
49-114: Fix presenter injection pattern and replace allfindViewByIdwith View Binding.This Activity violates two key guidelines:
Presenter injection: Replace
@InjectPresenterand@ProvidePresenterwith themoxyPresenterdelegate pattern (used consistently in MainActivity, LoginUserActivity, and other Activities).View references: Replace all
findViewByIdcalls with properties fromActivityExoplayerVideoBinding. The Activity already uses View Binding forplayerView(line 123) but then mixesfindViewByIdcalls fordataLoadingContainer,exoDebugLayout,serenityDebugTextView, and the debug helper—these should all come from the binding object.♻️ Suggested changes
-import moxy.presenter.InjectPresenter -import moxy.presenter.ProvidePresenter +import moxy.ktx.moxyPresenter @@ - `@InjectPresenter` - lateinit var presenter: ExoplayerPresenter - `@Inject` lateinit var presenterProvider: Provider<ExoplayerPresenter> + internal val presenter by moxyPresenter { presenterProvider.get() } @@ - `@ProvidePresenter` - fun providePresenter(): ExoplayerPresenter = presenterProvider.get() @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityExoplayerVideoBinding.inflate(layoutInflater) setContentView(binding.root) playerView = binding.playerView - - dataLoadingContainer = findViewById(R.id.data_loading_container) - exoDebugLayout = findViewById(R.id.exo_debug_layout) - serenityDebugTextView = findViewById(R.id.serenity_debug_text_view) + dataLoadingContainer = binding.dataLoadingContainer + exoDebugLayout = binding.exoDebugLayout + serenityDebugTextView = binding.serenityDebugTextView
121-129: ReplacefindViewByIdwith View Binding for direct layout views.You already inflate
ActivityExoplayerVideoBinding; use it forexoDebugLayoutandserenityDebugTextView. However,dataLoadingContaineris inside an included layout without anandroid:id, so it must remain afindViewByIdlookup. To make it accessible via binding, addandroid:id="@+id/data_loading_container"to the include element in the layout XML.♻️ Suggested change
val binding = ActivityExoplayerVideoBinding.inflate(layoutInflater) setContentView(binding.root) playerView = binding.playerView - dataLoadingContainer = findViewById(R.id.data_loading_container) - exoDebugLayout = findViewById(R.id.exo_debug_layout) - serenityDebugTextView = findViewById(R.id.serenity_debug_text_view) + exoDebugLayout = binding.exoDebugLayout + serenityDebugTextView = binding.serenityDebugTextView + dataLoadingContainer = findViewById(R.id.data_loading_container)
🤖 Fix all issues with AI agents
In
`@serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java`:
- Around line 122-123: The call to
context.getSystemService(Context.UI_MODE_SERVICE) can return null, so update the
AndroidHelper logic around uiModeManager and the isTv computation to null-check
the returned UiModeManager before calling getCurrentModeType(); if uiModeManager
is null, set isTv to false (or a safe default) and log or handle the missing
service. Locate the uiModeManager declaration and the isTv assignment in
AndroidHelper and guard the getCurrentModeType() call with an explicit null
check to avoid a NullPointerException.
In
`@serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt`:
- Around line 60-66: Rename the test function `init preseter updates client with
port` to reflect the null-port scenario being tested (e.g., `init presenter
defaults port to 8096` or `init presenter updates client without port`) so the
name matches the behavior where `mockServer.port` is stubbed as null; update the
test function name surrounding `presenter.initPresenter(mockServer)` and the
verification of `mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/")`
accordingly to keep intent clear.
🧹 Nitpick comments (2)
serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java (1)
110-114: Consider removing the redundantContextparameter.The method accepts a
Context contextparameter, but the class already has acontextfield (initialized in the constructor) that all other methods use. This parameter shadows the field and creates inconsistency.Suggested refactor
- public boolean isTclAndroidTv(Context context) { - String manufacturer = - android.os.Build.MANUFACTURER != null ? android.os.Build.MANUFACTURER.toLowerCase() : ""; - String brand = android.os.Build.BRAND != null ? android.os.Build.BRAND.toLowerCase() : ""; - String model = android.os.Build.MODEL != null ? android.os.Build.MODEL.toLowerCase() : ""; + public boolean isTclAndroidTv() { + String manufacturer = + Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase() : ""; + String brand = Build.BRAND != null ? Build.BRAND.toLowerCase() : ""; + String model = Build.MODEL != null ? Build.MODEL.toLowerCase() : "";Also update line 122 to use
this.context:- UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + UiModeManager uiModeManager = (UiModeManager) this.context.getSystemService(Context.UI_MODE_SERVICE);This aligns with the pattern used by other methods in this class (e.g.,
isAndroidTV(),isLeanbackSupported()). Additionally, use the importedBuildclass instead of fully qualifiedandroid.os.Build.serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (1)
480-551: Consider limiting bitrate capture to audio/video tracks.Right now any selected track can overwrite
bitrate, so a subtitle (or audio) track may replace the intended video value. If the overlay is meant to represent playback bitrate, restrict to audio/video and/or take the max.♻️ Suggested change
- if (format.bitrate > 0) { - bitrate = format.bitrate - } + if ((trackGroup.type == C.TRACK_TYPE_VIDEO || trackGroup.type == C.TRACK_TYPE_AUDIO) && + format.bitrate > 0 + ) { + bitrate = maxOf(bitrate, format.bitrate) + }
| UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); | ||
| boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; |
There was a problem hiding this comment.
Potential NullPointerException: getSystemService() can return null.
context.getSystemService(Context.UI_MODE_SERVICE) may return null on certain devices or configurations. Calling getCurrentModeType() without a null check will cause a crash.
Proposed fix
// Verify it is actually a TV device (Leanback)
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
- boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+ boolean isTv = uiModeManager != null
+ && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); | |
| boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; | |
| UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); | |
| boolean isTv = uiModeManager != null | |
| && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; |
🤖 Prompt for AI Agents
In
`@serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java`
around lines 122 - 123, The call to
context.getSystemService(Context.UI_MODE_SERVICE) can return null, so update the
AndroidHelper logic around uiModeManager and the isTv computation to null-check
the returned UiModeManager before calling getCurrentModeType(); if uiModeManager
is null, set isTv to false (or a safe default) and log or handle the missing
service. Locate the uiModeManager declaration and the isTv assignment in
AndroidHelper and guard the getCurrentModeType() call with an explicit null
check to avoid a NullPointerException.
| fun `init preseter updates client with port`() { | ||
| every { mockServer.ipAddress } returns "192.168.0.1" | ||
| every { mockServer.port } returns null | ||
|
|
||
| presenter.initPresenter(mockServer) | ||
|
|
||
| verify { mockSerenityClient.updateBaseUrl(any()) } | ||
| verify { mockServer.ipAddress } | ||
| verify { mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/") } |
There was a problem hiding this comment.
Rename test to reflect null-port behavior.
The test name says “with port” but now stubs port as null. Consider renaming to “without port” or “defaults port to 8096” for clarity.
🤖 Prompt for AI Agents
In
`@serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt`
around lines 60 - 66, Rename the test function `init preseter updates client
with port` to reflect the null-port scenario being tested (e.g., `init presenter
defaults port to 8096` or `init presenter updates client without port`) so the
name matches the behavior where `mockServer.port` is stubbed as null; update the
test function name surrounding `presenter.initPresenter(mockServer)` and the
verification of `mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/")`
accordingly to keep intent clear.
|
Closes #430 |



Adds a Debug Overlay for video playback that contains general informat for the state of playback. Also fixes an issue with the app crashing when the user login can not connect to the server.
Summary by CodeRabbit
New Features
UI
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.