Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.WindowInsets;
Expand Down Expand Up @@ -82,7 +83,7 @@ public class Decoder extends Activity
private boolean quadEnabled;
private QuadCell[] quadCells;
private LinearLayout quadContainer;
private final TextureView[] quadViews = new TextureView[4];
private final SurfaceView[] quadViews = new SurfaceView[4];
// Quad audio (#12): the cell whose sound plays, -1 for none
private int quadAudio = -1;
private final TextView[] quadAudioBadges = new TextView[4];
Expand Down Expand Up @@ -245,7 +246,7 @@ public void onCodecChanged(boolean h265, AudioConfig audio) {
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
for (int col = 0; col < 2; col++) {
int idx = row * 2 + col;
quadViews[idx] = new TextureView(this);
quadViews[idx] = new SurfaceView(this);
quadViews[idx].setOnClickListener(cv ->
menuManager.showMenu(menuAnchor, quadEnabled, mActive, mVersion,
BuildConfig.GIT_HASH, mHosts));
Expand Down Expand Up @@ -675,7 +676,7 @@ private void removeClearOverlay() {

private void clearQuadViews() {
for (int i = 0; i < 4; i++) {
TextureView t = quadViews[i];
SurfaceView t = quadViews[i];
if (t == null) continue;
FrameLayout cell = (FrameLayout) t.getParent();
View overlay = cell.findViewWithTag("quad_overlay_" + i);
Expand All @@ -695,7 +696,7 @@ private void clearQuadViews() {

private void removeQuadOverlay(int index) {
if (index < 0 || index >= quadViews.length || quadViews[index] == null) return;
TextureView t = quadViews[index];
SurfaceView t = quadViews[index];
View overlay = ((View) t.getParent()).findViewWithTag("quad_overlay_" + index);
if (overlay != null) {
overlay.setVisibility(View.GONE);
Expand Down
75 changes: 31 additions & 44 deletions android/app/src/main/java/com/openipc/decoder/QuadCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
Expand All @@ -22,7 +23,7 @@ class QuadCell implements RtspSession.Callback {

private final int index;
private final String host;
private final TextureView view;
private final SurfaceView view;
private final String tag;
private final FramePool cellFramePool;
private final String userAgent;
Expand Down Expand Up @@ -65,7 +66,7 @@ void setAudioSink(AudioManager sink, int route) {

private final Listener listener;

QuadCell(int index, String host, TextureView view, FramePool pool, String userAgent, Listener listener) {
QuadCell(int index, String host, SurfaceView view, FramePool pool, String userAgent, Listener listener) {
this.index = index;
this.host = host;
this.view = view;
Expand Down Expand Up @@ -102,51 +103,36 @@ void start() {
started = true;
running = true;
decoder.setEnabled(true);
view.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(android.graphics.SurfaceTexture st, int w, int h) {
if (threadsStarted) return;
Surface old = surface;
surface = new Surface(st);
if (old != null) {
try { old.release(); } catch (Exception e) {
Log.e(TAG, tag + " Error releasing old surface", e);
}
}
startThreads();
}
@Override
public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture st, int w, int h) {
Surface old = surface;
surface = new Surface(st);
if (old != null) {
try { old.release(); } catch (Exception e) {
Log.e(TAG, tag + " Error releasing surface on size change", e);
}
}
}
@Override
public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture st) {
Surface s = surface;
surface = null;
if (s != null) {
try { s.release(); } catch (Exception e) {
Log.e(TAG, tag + " Error releasing surface on destroy", e);
}
}
return true;
}
@Override
public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture st) {}
});
if (view.isAvailable()) {
if (surface == null) {
surface = new Surface(view.getSurfaceTexture());
}
// SurfaceView, not TextureView (#61): with a TextureView every decoded frame raises a
// frame-available callback that the framework delivers by attaching a native thread to
// the VM, which allocated about 1 MB/s of garbage with four streams
view.getHolder().addCallback(holderCallback);
Comment on lines +106 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Quad playback still allocates 📎 Requirement gap ➹ Performance

The new SurfaceView path leaves QuadCell.startThreads() and the decoder/audio hot paths
unchanged, including the repeated timed queue poll. During four-stream playback, the supplied
measurement still records 7–10 KB/s per decoder thread and 5 KB/s in audio, so Quad playback does
not meet the allocation-free hot-path criterion.
Agent Prompt
## Issue description
Changing the Quad renderer to `SurfaceView` removes the framework frame-available callback allocations, but the decoder and audio paths still retain measured hot-path allocations during four-stream playback. Profile the updated path and remove or pool the remaining allocations so playback does not rely on repeated garbage collection.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[106-109]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[132-150]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[184-205]

## Recommended Fix
Run allocation profiling during four-stream Quad playback after the `SurfaceView` change, then eliminate the remaining decoder, queue, and audio hot-path allocations by reusing buffers and avoiding per-iteration temporary objects. Verify that large video frames and audio buffers remain within the existing pool and that blocking young garbage collections no longer recur because of Quad playback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Surface s = view.getHolder().getSurface();
if (s != null && s.isValid()) {
surface = s;
if (!threadsStarted) startThreads();
}
}

private final SurfaceHolder.Callback holderCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
// The surface belongs to the SurfaceView: it is used, never released, here
surface = holder.getSurface();
if (!threadsStarted) startThreads();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Nothing may render into the surface once this returns, and a recreated surface
// needs a decoder configured for it: close the decoder, the decode thread creates
// a new one when the next surface is there
surface = null;
decoder.closeDecoder();
}
};

private void startThreads() {
if (threadsStarted) return;
threadsStarted = true;
Expand Down Expand Up @@ -193,6 +179,7 @@ void stop() {
executor = null;
}
nalQueue.clear();
if (view != null) view.getHolder().removeCallback(holderCallback);
surface = null;
}

Expand Down
Loading